问题

为什么有32个关卡的游戏《超级马里奥兄弟》只要40KB?

回答
这个问题很有意思,它触及到了早期电子游戏开发中一个非常关键的方面:资源的极限运用。你说的没错,《超级马里奥兄弟》(Super Mario Bros.)这款在1985年发布的任天堂FC(Famicom,即红白机)上的经典游戏,其卡带容量仅仅是区区40KB(或者说0.04MB)。这在今天看来简直不可思议,因为现在一张高清图片可能都远超这个大小。

那么,它是怎么做到在这么小的空间里塞下32个精心设计的关卡、敌人、道具、背景音乐、音效,还有流畅的操作手感和那个标志性的马里奥跳跃动画的呢?原因可以从以下几个方面来详细拆解:

1. 极致的代码优化与算法设计:

精简的指令集和高效的编程语言: FC使用的是一款相对简单的CPU,程序员需要用汇编语言进行开发。汇编语言直接对应机器码,允许开发者对内存和CPU操作进行更底层的控制,这意味着每一条指令都需要精确计算,不允许丝毫浪费。他们会寻找最有效率的代码写法,比如用更少的指令完成同一个任务。
过程生成和重复利用的智慧: 关卡的构建并不是像我们现在这样一张一张“画”出来的。早期的游戏开发者会设计一套“规则”或“算法”,让关卡中的某些元素(比如砖块、问号方块、敌人、平台)按照预设的模式进行组合和排列。这样一来,开发者只需要存储生成这些模式的“种子”或“参数”,而不是每一关卡的所有具体信息。
块状化设计(Tilebased design): 游戏画面是由许多小的“图块”(tiles)组成的。这些图块是预先设计好的图像单元,比如一块砖头、一个平台、一朵云、一个敌人模型的一部分。游戏引擎会将这些图块按照关卡的布局指令进行拼接,形成完整的画面。这比存储每一帧完整的图像要节省巨大空间。
重复利用素材: 很多关卡元素是高度重复的。比如,你看到的无数个砖块,其实都来自于同一个砖块的图块数据。敌人,比如普通库巴、陆行鸟(Goomba)、乌龟(Koopa Troopa),它们的模型数据也是被重复调用和稍作调整的。
状态压缩: 游戏中的敌人在不同状态下(例如乌龟壳在滚动、被踢飞、正常行走)可能只需要少量的数据来描述差异,而不是存储完整的独立模型。

2. 数据压缩技术的早期应用:

游程编码(RunLength Encoding, RLE): 这是一种非常基础但有效的压缩技术。如果有一串连续相同的像素数据,比如“白色白色白色白色白色”,RLE可以将它表示成“5个白色”。在游戏的图像和关卡数据中,重复出现的颜色块或元素组合非常普遍,RLE可以有效减少数据量。
自定义数据结构和编码: 开发者会设计专门的数据结构来存储游戏中的各种信息,比如关卡布局、敌人属性、道具类型。他们会使用更紧凑的编码方式来表示这些信息。例如,一个敌人可能只需要几个字节来描述它的类型、起始位置和行为模式,而不是存储一个庞大的对象定义。

3. 美术和音效的取舍与优化:

低分辨率和有限调色板: FC时代的显示能力非常有限。游戏的画面是低分辨率的,而且可用的颜色数量也非常少(通常是有限的几种调色板)。这意味着美术素材本身占用的空间就非常小。
音乐和音效的简化: 游戏中的背景音乐使用的是合成的波形(waveforms)和简单的音符序列,而不是采样高质量的音频。音效也是如此,比如马里奥跳跃的“跳”声,可能就是一个简单的波形振动叠加。这些数据的体积都非常小。
旋律数据而非采样: 音乐不是录制的音轨,而是由一系列指令组成的,告诉CPU演奏哪个音符、持续多久、使用哪种乐器(音色)。这种指令集的体积远小于音乐样本。
有限的音色库: FC只能模拟有限几种音色,比如简单的脉冲波、锯齿波等。这些音色数据本身就很小。
动画帧数的控制: 角色和敌人的动画并不是极其流畅和精细的。它们是通过有限数量的帧画面组合而成的,通过巧妙的设计,即使帧数不高,也能给人流畅的视觉感受。

4. 游戏设计的哲学与限制:

聚焦核心玩法: 《超级马里奥兄弟》的核心就是平台跳跃和躲避敌人。它没有复杂的剧情叙事,没有大量的过场动画,也没有开放世界的探索。游戏的重点放在提供极具挑战性和趣味性的关卡设计上。
利用玩家的想象力: 早期游戏很多时候会留下一些“留白”,让玩家通过想象力去填补。比如背景中的一些元素,可能只是为了营造气氛,并不需要非常详细和独特的数据。

总结一下,为什么《超级马里奥兄弟》能做到这么小?

你可以想象成一个非常有经验的建筑师,要在非常有限的土地上建造一座大型的、功能齐全的城市。他会:

重复使用建筑模块: 像预制件一样,用同样形状的窗户、门、墙壁来建造不同的房屋。
精打细算空间: 规划好每一寸土地的用途,尽量让建筑结构紧凑而有效。
使用轻便的材料: 建筑材料本身不会太重,不会占用太多空间。
依靠巧思而非堆砌: 通过巧妙的设计和布局,让城市显得丰富多彩,而不是简单地堆砌大量的建筑。

在《超级马里奥兄弟》的例子里:

建筑模块 就是那些图块。
空间利用 就是代码优化和算法设计。
轻便材料 就是低分辨率美术和压缩后的数据。
巧思 就是关卡设计的精妙之处和玩家的代入感。

所以,40KB之所以能承载如此丰富的游戏内容,是早期游戏开发者们在极度严苛的硬件限制下,通过极致的编程技巧、巧妙的数据设计和对资源的高度整合,将每一个字节都用到了极致的体现。这不仅仅是技术上的胜利,更是对游戏设计艺术的深刻诠释。

网友意见

user avatar

早期红白机的经典游戏容量出乎意料的小。

这个问题曾经在知乎上引起过热烈的讨论,B站、微信上都有相关的热门视频。相关资料我会整理到答案的末尾,以供参考。

经过一段时间的广泛讨论,这一问题逐渐变得更加清楚了。感谢Anjie的邀请,再将这个问题整理一遍做出回答,对一些有价值的信息做个总结 :)

1、核心压缩方法——Tile(瓦片)

首先,实际上初代《超级马力欧兄弟》只有40KB。

我们先别急着惊讶,先问一个问题:无论40KB还是400KB,它一定有一种基本的压缩方法,这个压缩方法与我们今天保存图片的方式肯定从根本上就有区别。

红白机的基本图像单元为“Tile瓦片”,每个瓦片为8x8像素大小。与现代游戏直接绘制像素的思路不同,红白机上的游戏必须先准备好一系列瓦片,再把瓦片拼在一起形成画面。

为什么要这么做呢?通过简单的数学运算可以知道,先做一些瓦片再拼接瓦片,内存占用量要远远低于直接绘制每一个像素。

例如:一张128*128像素的图片,每个像素可以选2^8=256种颜色,那么每个像素需要记录8bit=1Byte信息,共占用空间16KB。(128*128*1 Byte)

而如果换成8x8的瓦片去铺满图片,则只需要 16 * 16 = 256个瓦片就够了,如果总共只有256种不同样式的瓦片,那么只需要256B 即可表示这张图片。

简单来说:用重复出现的模式拼成一幅画面,可以极大压缩图片占用的空间

但这里留下了一个问题:256种瓦片本身也要占用空间,瓦片本身如何存储?

2、FC如何保存色彩数据

FC虽然画质简陋,但色彩还是相当鲜艳的。在当时的技术条件下,FC理论上可以表示50多种颜色,一个像素的颜色可以用6bit表示。而一个瓦片有8x8 = 256个像素,那么就需要 8x8x6 bit = 48 Byte 来表示一个瓦片。

这么算起来就出问题了~~ 256种瓦片,每个瓦片 48 Byte,瓦片容量等于256*48 B = 16384 B。好家伙,什么都没干,光瓦片就存了16KB,显然太多了。

解决这一矛盾的核心,是另一个问题:FC如何保存颜色数据?

通过 @文礼 大神的回答可以知道(文礼 的回答),每个瓦片只能绑定一个调色板,而每个调色板只有4种颜色,所以每个瓦片的容量占用仅有 8x8x2bit = 16Byte,比上述估算的少4倍

而且,调色板带来另一个有趣的功能,考虑下面几个图片:

外观完全一样有没有?

上图中不同的图片仅仅是颜色不同,并不需要创建新的瓦片,只需要给同一个瓦片替换不同的“调色板”即可。这样可以巧妙利用调色板,创建出不同皮肤的物体,而容量几乎没有增加。

3、FC如何保存音乐数据

现代音乐格式往往直接保存声道的波形,这种做法保真度高、通用性强,但很显然占用空间多,一首曲子的容量以兆字节计算。

而八位芯片时代的音频解决方案,关键是一颗专用芯片,例如FC用的理光2A03:

音频芯片可以产生合成音效,能提供的音色可以在一定程度上配置,但非常有限。听听FC游戏的音乐可以体会到常用的音色几乎一样。我觉得这个音频芯片最厉害的地方是可以同时播放几个音轨(但不能是和弦那种“同时”),《魂斗罗》、《沙罗曼蛇》、《忍者龙剑传》的殿堂级音乐,主要是靠多个音轨的交替配合实现的。

每个音符只要记录音色、频率和音高就足够了,音频芯片会识别出来。把音符按时间排列好就是“乐谱”了,可以简单理解为“简谱”。这种简谱需要的数据量十分有限,而且大部分游戏音乐都是循环播放,数据量更是小的可怜。

当时的芯片擅长产生的波形包括方波、三角波、正弦波等等,其中三角波用来做Bass效果很好。而《超级马力欧兄弟》里面的“鼓”是用“噪波”实现的,也是当年的常用做法。

FC的音频芯片还支持短时间的采样音乐,后期的《忍者龙剑传3》BGM里面的鼓声采用了8Bit采样声音, 效果超棒。

4、FC的程序代码容量

有趣的是,现在绝大多数人都忽略了程序代码本身所占用的空间。但在那个内存容量极其有限的年代,代码的体积不可小觑。

FC时代的游戏虽然逻辑不可谓不丰富,但确实整体代码不大。为什么呢?

因为FC时代的游戏,没有所谓的“引擎层”,FC的硬件本身就是一个非常简陋的游戏引擎。任天堂的主机完全是为游戏而设计的,瓦片、调色板、音乐、音效等基本功能已经预先考虑到了,使用特定的方式就能直接调用,这样一来就节约了大量底层代码。

程序员要仔细研究文档,在硬件框架下思考问题,比如如何显示图片、如何卷动屏幕等等;而且还要非常熟悉硬件底层和汇编,不要浪费代码空间。

一来二去,代码也能写的非常小。

5、其它优化机制

为了极限压榨容量,当年的程序员和美术也动了不少脑筋,比如几个经典案例:

这些细节的优化,也对压缩大小起到了一定作用。但总的来说,并不是让容量变小的主力~~~

总结

以上分5个部分,详细介绍了FC游戏容量小的原因,特别是背后的本质原理。

以上内容参考了很多内容,包括不限于wiki、知乎、B站等渠道。有错误的地方不吝赐教~~

参考资料:

  1. 知乎问题:为什么魂斗罗只有128KB却可以实现那么长的剧情?
  2. B站:【差评君】为什么有32个关卡的超级马里奥兄弟只要40KB?_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
  3. wiki:en.wikipedia.org/wiki/S.

还有其它很多参考,不好一一列举,向这些作者表示感谢。

user avatar

首先,为什么是40KB?

因为FC在硬件上,CPU允许一次性映射ROM中32KB的数据(感谢指正,并非是读入内存而是映射,即分配一个可访问的地址),而PPU则是一次能用8KB。如果超过这个容量,也不是不可以,只是无法一次性将整个ROM中的数据映射到地址空间,而是需要动态切换,在需要的时候,将ROM中的特定数据映射(这叫做切换bank)。超级马里奥兄弟1代没有使用这样的技术,所以最大可用的容量就是32+8=40KB。

其他回答主要讲到了FC对于图像的处理,这其实是所有FC游戏(以及很多传统游戏机上的2D游戏)的处理方法。也就是说,这种压缩是通用的,并非超级马里奥兄弟独有。至于广为人知的“树丛和云使用的相同的图像”这一点,其实很有趣,因为它们的tile虽然确实一样,但使用了两个不同的meta-tile。

什么是meta-tile呢?

把4个8x8的tile组合起来,就是一个meta-tile。meta-tile的大小显然是16x16。用meta-tile的好处是,原本一个(比方说)32x32的图像只需要用4个meta-tile来描述,而原本需要用16个tile来描述。虽然我们需要额外的空间来存储meta-tile,但是meta-tile是可以公用的,不会占据额外的空间。

已经有人反编译出了可读的马里奥1代源代码(注意是可读的。简单地将FC的二进制数据反汇编成6502汇编码很容易,但那是不可读的)。从中可以看出,云和树丛使用的meta-tile不同,尽管它们的tile一致:

       ...... Palette0_MTiles:   .db $24, $24, $24, $24 ;blank   .db $27, $27, $27, $27 ;black metatile   ;这里是树丛   .db $24, $24, $24, $35 ;bush left   .db $36, $25, $37, $25 ;bush middle   .db $24, $38, $24, $24 ;bush right   .db $24, $30, $30, $26 ;mountain left   .db $26, $26, $34, $26 ;mountain left bottom/middle center   .db $24, $31, $24, $32 ;mountain middle top ...... Palette2_MTiles:   ;这里是云,注意云有下半部分,树丛没有   .db $24, $24, $24, $35 ;cloud left   .db $36, $25, $37, $25 ;cloud middle   .db $24, $38, $24, $24 ;cloud right   .db $24, $24, $39, $24 ;cloud bottom left   .db $3a, $24, $3b, $24 ;cloud bottom middle   .db $3c, $24, $24, $24 ;cloud bottom right   .db $41, $26, $41, $26 ;water/lava top ......     

至于为什么要分两个meta-tile,可能是因为需要使用不同调色板的原因。


这个回答不打算涉及更多的关于图像的方面,毕竟图像只有8KB(实际上还有几十个字节空余),而前面的程序代码有32KB,而且是一个字节也没有空余了(尽管中间还是有几个字节的多余代码)。而且,问题中提到了“32个关卡”,所以我打算主要说一下32个关卡对空间的占用。

32KB的代码当中,32个关卡占用了4647字节,约占总代码量的1/7。可能有人会很意外:32个内容丰富的关卡的数据,比游戏运行代码的数据量更少吗?事实上,马里奥1代的代码不算简单,光马里奥,就需要处理走、跑、蹲、转身、跳跃、游泳、顶砖、爬藤、发射火球、进水管(纵向和横向都有),还有与物体的互动,如踩怪、弹簧跳、与各种移动平台交互等。现代的游戏的这些细节都可以交给引擎来做,但FC时代并没有游戏引擎,所有的这些都需要自行处理。并且除了代码,还有音乐和文本,以及前面提到的meta-tile数据,都算在这32KB里了(尽管这些加起来也没有关卡数据多)。

怎样用4647字节描述出全部32个关卡(包括各种奖励区域在内)呢?我们以1-1举例。

马里奥1代的关卡,分为敌人和地形两部分,分开存储。1-1的敌人占用30字节,地形占用101字节(不含地下奖励区域,这个后面会提到)。

如果你玩《马里奥制造》,你可能会误以为马里奥1代的地图是一格一格存储的。但对于马里奥1代来说,连图像都要用tile、meta-tile、实际显示这样三级压缩,一格一格存储地图实在太过奢侈。

首先,我们发现整个地面都是2格高的,这意味着不需要把整个地形存储下来,只要存储“整个关卡都是2格高的地面”,就行了。马里奥1代设置了16种地形,这种2格高的地面就是其中一种,其他的还包括:没有地面(如1-3使用),下面2格高地面+天花板1格封死(如1-2、1-4使用)等等。16种地形,只占用半个字节:也就是说我们用半个字节就存储了整个1-1的地面。只是我们需要在地面上挖3个坑而已。

然后,我们关卡中的一个物体,其实只要2个字节:位置以及类型。

一个关卡这么长,怎么只用一个字节就能描述位置呢?我们注意到1代所有关卡都是1屏幕高的,而一个物体的大小是16x16,所以纵坐标只要16格就足够了(屏幕是240像素高,我们凑个整,用256像素)。而横向上,我们把每16格划分为一“页”。这样每一页都只有256个格子,刚好用1个字节就够了。

那么不同的页怎么办呢?我们可以在某个物体上标记一个“换页”。游戏读取到这个标记后,就表示在这以后的物体都是在下一页(或者下一页以后),不用在这一页加载这些物体了。这个标记只要1个bit就行了(只有“换页”和“不换页”两种),可以从物体类型的那个字节挤出来:毕竟马里奥1代不需要256种物体类型,去掉1比特后,剩下128种也足够了。

我们发现物体不会埋在地下,所以纵坐标有些位置是用不到的。这可以用来设置一些特殊物体:比如坑,它不需要纵坐标,我们就不需要浪费描述纵坐标的那半个字节,而那半个字节完全可以描述“坑的大小”。又比如,如果有一整页都没有物体,要怎么描述呢?我们也可以用一个特殊的“跳页”物体来描述。这个物体显然也不需要横坐标和纵坐标。特殊物体还有个好处:它可以不占用那128种物体的数量,因为它和普通物体是分开处理的。

而物体的类型,则还包括了物体的大小。比如关卡的中间部分一长排的横向砖,其实并不是很多个物体并排,而是一个“长度为8的横向砖块”。这样一来,128种物体还够用吗?不够。但没事,马里奥1代做了很多特殊处理,强行压缩到了128种不同的物体,比如:

  • 炮台和蘑菇平台,其实是同一种物体,只是在不同风格的关卡中显示不同而已。
  • 地面和地下场景可以顶碎的砖块,在水下会变成不可顶碎的紫色珊瑚,它们也是同一种物体。
  • 有些地方有一长排的?砖块,但这种砖块是特殊物体,不能设定它的纵坐标。它只能在2种特定的纵坐标出现。作为特殊物体,它不占用128种物体的数量。
  • 水管只有“可进入”和“不可进入”两种,最高高度是8,所以水管物体只有16种。至于水管会通向哪里,则不是和物体存储在一起,而是和敌人的数据存储在一起的:游戏读取了目标位置后,所有水管和藤蔓都会把你传送到那个目标位置,直到游戏读取下一个目标位置或者进入下一关为止。(这也是马里奥1代速通当中,4-2进水管却到了爬藤蔓到达的跳关区域的原理。)
  • 很多不需要纵坐标的物体也是特殊物体。如库巴的桥、斧头、旗杆、城堡等等。

还有各种各样节省关卡容量的特殊手段:

  • 1-1后半段出现了2个4格高的台阶,这种台阶也是特殊物体,不是用4个物体拼起来的。旗杆前的8格台阶+右侧多一列的组合也是一整个特殊物体。
  • 1-2有大量复杂的砖块,但是他们并非是很多砖块物体拼接而成的,而是不停地在16个地形之间切换。
  • 1-1一开头有个“砖-蘑菇砖-砖-金币砖-砖”的设计,这里不是5个物体,而是一个5格长的横向砖,叠上2个?砖,总共3个物体。

最后,这个关卡的最前面,还有两个字节,表示关卡的整体属性,比如:

  • 关卡一开始,是16种地形中的哪一种?(就是上面提到的半个字节)
  • 关卡是什么类型的?(地面,地下,水下,城堡)
  • 关卡有多少时间?(400、300、200)
  • 关卡的背景是什么?(1-1是山丘和树丛,1-3是天空,而1-2的地下部分则什么都没有)
  • 关卡是什么风格的?(比如第5世界其实是下雪的世界,第3世界是黑夜等等。还有上面提到的,关卡中到底是炮台还是蘑菇平台?)

关卡最后则是1个字节的结束标志。

敌人的设计也是类似的,这里就不详细讲了。除了固定放置的敌人,还有一种不停产生的敌人,比如2-3的飞鱼,5-3的炮弹等等。另外,水管里的食人花不需要专门设置:除了1-1,其他关卡每个水管里都有食人花。有时候没有食人花,是因为马里奥1代最多同时出现5个敌人(严格地说是除了马里奥以外的sprite),超出部分会被丢掉。

最后的最后,还有一点,就是关卡重用。比如奖励区域,很多关卡都会到相同的奖励区域(上述的1-1就有一个),这些区域其实也是重用的。还有一个更绝的关卡重用:1-2、2-2、4-2、7-2从地下或水下出来到地上,直到拉旗子的那一段,其实都是1-1的结尾。

有些关则是完全相同的,比如1-4和6-4,1-3和5-3等等,但是其中的敌人有所不同。但是没必要设置两套敌人,只要让一部分敌人在前面的那关不出现,在后面的那关出现就行了。马里奥1代有个分界线:5-3,所有这种相同的关卡中的敌人,都是区分在5-3之前出现,和在5-3之前不出现的。

总之,通过各种压缩手段,使马里奥1代的32个关卡的数据只占用了4647个字节。事实上游戏代码才是40KB中的大头。

user avatar

首先,超级马里奥兄弟1(以下略做SMB1)的尺寸是40KB而不是64KB,32K的PRG(程序)和8K的CHR(图像)。

图像素材只有固定的几种“块”(Tile),地图可以进一步简化成表示了特定块组合(2x2)的二维数组,一格16x16像素。

1-1的尺寸只有212*13格,一格一个字节来算,也就是2756字节。但如果按照3k左右来算32个关卡容量肯定不够(光地图数据都有96KB了)。所以SMB1首先把对象做了抽象化,定义了一些“对象”,比如“砖块”、“管道”、“城堡”之类的,然后关卡数据仅保存这些“对象”的出现位置,一个对象占据2字节。连背景加敌人以及金币等数据,1-1的尺寸在131个字节,其他关卡基本也没有超过200个字节,这样一来32个关卡合计容量也就在7KB以内,外加几百个字节的字符和其他信息,剩下至少24KB可以用来存储程序代码。6502作为一个8位CPU,指令都不长,24KB可以作为代码来说是一个相当大的容量。

而FC的一个Tile(8x8像素)最多只能显示4种颜色,至于哪4种则可以自由选择(称作调色板),所以一个像素可以用2比特来表示,每8x8的图形占用16个字节。这个数据是不包含调色板信息的,调色板是程序动态指定的。虽说8KB的尺寸限制之下可以用的块只有512个,但是SMB1通过活用调色板也达到了多彩多样的图像。而且如上文所说,程序上把最基础的8x8像素的“块”抽象定义成了16x16像素的“块组合”,在每一关(也有可能是整个游戏)不超过128种块组合,并且每一关固定10种颜色组合成4种调色板,这样一个字节就可以代表一个块组合(2比特调色板+6比特块组合id),再进一步抽象定义“物件”,一个物件可以有多个块组合来构成。这样多层抽象的顶一下就可以大幅度节约内存,因为你的一个对象并不记录所有的块信息和他们的颜色。

于是,在这样的手法加持下,SMB1做到了40KB的限制下达到了极高的可玩性,提供了32个多彩多样的关卡。

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有