计算机门外汉特地请教。
想当初啊,电脑的图形芯片没有独立的显存,都是从主内存里划一片区域出来保存屏幕上所显示的数据的。那得划多大的空间呢?
拿常见的 320×200 分辨率来说:如果只支持纯黑白显示(比特位 0 表示黑,1 表示白),那么就需要 320×200×1 比特,也就是 8K;而如果要支持简单的 16 色显示,那么就得占 320×200×4 比特,也就是 32K。
不过当时的电脑内存也就 16K、32K 的样子,光支持屏幕显示占掉整个内存的话还让人怎么活呀,所以大家就得想办法压缩。
于是聪明人想到,既然电脑屏幕上显示的主要都是文字,那不如就一个字符一个字符地来指定颜色。一个字符占 8×8 的位置的话,就把屏幕划成 40×25 的格子,每个格子可以指定一个前景色和背景色不就可以满足绝大部分情况的需求了吗?
这样支持 16 色就只需要占用 320×200×1+40×25×4 比特,也就是 8.5K 的空间了。(如果只是文本模式,不需要精确到每个像素,每个格子只存一个字符、一个前景色、一个背景色,还可以省更多。)
这样做的坏处是根本没法精确地显示图片。于是为了解决这个问题,不少电脑(游戏机)都内置了特殊的硬件,可以在主内存之外单独放置一些比较小的固定宽高的图片数据。显示的时候,主内存里的数据就先显示到屏幕上,然后再把这些硬件里单独的图片覆盖显示到屏幕上的指定位置。
这样分层显示,你就会看到这些单独的一张张图片「飘浮」在正常的文字信息之上,不受其影响,还可以出现在任意位置,跟幽灵一样,所以大家亲切地称之为精灵(Sprite)。
后来电脑的内存大了,显存也独立了,一般就没有硬件精灵了。但术语还是继承了下来,只要是我把一张图片画到主图片上,那么这张图片就是精灵。
--
更新:
红白机甚至更早的雅达利主机上就有专门的硬件精灵了(而且就叫这个名字),并不需要等到 Windows 的 GDI 出现。
因为是硬件,所以和主内存里图像的合成是直接在生成扫描线时候做的,不需要 CPU 干预。
但后来没有硬件精灵了,用 CPU 去合成,精灵才和双缓冲(隐藏合成过程)、脏矩形(加速合成)之类的优化联系了起来。
嘿嘿,知乎故事会真有趣,这都能编出故事来,还像真的一样。但是为了让这种老的开发游戏的概念不被“知乎故事编辑”玷污了,我还是得花点力气,把这个历史片段留给你们。
首先你必须知道,精灵这个概念出现的时候,游戏开发早就已经不是单片机了(事实也是几乎没有正规游戏是单片机开发出来的),甚至游戏开发的时代,已经有了比较成熟的GDI(Graphic Device Interface)了,而精灵这个概念,是因为大量的平面俯视视角游戏,才诞生的。
在老的2D游戏开发中,有一个重绘的问题,其实今天也有这个问题——就是你屏幕上看到的东西,在大多的游戏中是每一帧都重绘的,确切的说重绘一次算是一帧(也就是Unity里面的Update),因为需要绘制的内容问题(当然不完全是图画导致的,主要还是贴图数量等)会导致绘制速度有所不同,所以Update很多时候不是固定的。需要重绘的区域面积越大,开销也就越大——不论你是几D游戏,不论是显卡还是芯片算,最终底层在做的事情,无非就是确定屏幕上每一个点的颜色(所以分辨率越高的情况下游戏也就越卡,因为要算的点的数量变多了),因此做优化的时候,最好的办法就是减少需要计算的点数总数。
而这个屏幕刷新,老的做过2D游戏的,都有过这样的经验——就是“脏区域重绘”,这个概念也流传到今天——简单的来说,就是当两帧之间屏幕有变化的地方,才进行重绘,而不是整个屏幕重绘。比如H5小程序(而非小游戏)的主流框架Vue和react等,他们的一些机制,也是为了解决脏区域重绘,比如useState(...)之类,再比如mobX框架等,都是为了数据变化时候,仅仅数据相关的控件的相关区域进行一次重绘,从而使得渲染效率极高化。这本身是一种优化做法,就像卡马克算法,是对于内存的一种优化。最早出现SpriteLayer(也就是精灵层)的概念,是在平面俯视视角的游戏里(这是一个2D特有的视角,比如FF2就是,3D是模拟不出来的,强行凑效果可以,但是代价很大):
在这样一个视角下的游戏里,很多时候场景并不用每一帧都重绘,因为场景里的东西也不会变化,甚至连动画都没有,因此实际上需要重绘的部分无非就是有动画的那些元件,比如角色、比如旋转的风车等。
因此我们把地形这一层和动画所在的层分开,因为地形所在层的重绘频率是远低于角色所在层的(角色因为有动画,每一帧都需要重绘,每一帧都更换为下一帧的贴图,你才能看到角色动起来,这也称为“序列帧动画”)
因此我们把这些每帧需要重绘的提出来在单独一层,而这些要重绘的Rect,也正好就是脏区域,因此这些元素就是Magical的——因为他们是一个特殊处理,让我们不用全屏幕重绘,而只要在这一层上设定某些位置重绘就行了。你所看到的屏幕,实际上是按层绘制出来的,有些RPG,如上面用的图里面,因为角色还会被东西遮挡,于是有了地面层、精灵层和天空层3层——先绘制地面层,再绘制精灵层,最后绘制天空层,天空层和地面层的重绘算法,依赖于卡马克算法优化后的做法(这个其实各家不同但都很相似),但是精灵层的重绘是每一帧都进行的。
所以啊,sprite怎么会是因为“漂在上面”而得名呢?漂在上面的叫ghost或者wisp。而如果你不明白这个意思,那就直说Sprite就是Sprite,大家习惯这么写也不过分(确实后来的人不关心为啥,只知道很多开发框架里角色就叫Sprite就这么叫了)。
看了现有的一些回答,都遗漏了一个重要方面:游戏发展早期,C甚至汇编当道;复杂的游戏逻辑(玩家操作、攻击技能、伤害判定、各种特殊的技能/状态逻辑,等等等等)想要有条不紊的管理起来、随时间流逝均匀可控的运作、同时又要尽量优化性能(比如其它答案谈到的“图形显示优化”)……
这难度,飞上天了吧?
因此,就有了“精灵”抽象:游戏里一切能感知时间流逝、具有坐标位置的,都是精灵。
你看,一下子抓住了两个根本:一、有坐标信息;二、能感知时间流逝。
一旦抓住这两个根本,事情就容易起来了。
我们可以设计一个定时器,周期性的调用和精灵绑定的处理函数(相当于C++的虚函数,接受的传入参数是interval)——实际上,这个处理函数是一个dispatcher,它负责转调用我们写的一切动画以及碰撞判定等逻辑的处理函数。
所有这些函数都要接受interval参数,比如让精灵按贝塞尔曲线运动的函数就是用interval乘以运行速率、然后改写精灵的坐标信息;同样的,动画呢,就是按照interval的节律缩放扭曲图片或者改变它的颜色(或者基于interval把一组图片换来换去,从而实现走路、放大招等效果)。
注意精灵是可以没有对应的图片的,比如我们要在进门处放个陷阱,那就是一个和门口坐标重叠的、不可见的“触发器trigger”,当碰撞检测发现游戏物体和这个“触发器精灵”坐标重合时,就会调用对应的trigger。
换句话说,这是用C甚至汇编实现的一个原始而精巧的继承体系;它的根是“精灵”这个基类,提供坐标这个公开数据成员和dispatcher时钟信息的接口;然后是“支持碰撞检测以及提供接受碰撞事件并加以处理的接口”的继承类,和“支持动画”的继承类以及其它各种继承类。
至于为什么叫“精灵”,大概是因为它只是能“感知”有位置,却未必有实体吧——在西方传说里,“精灵sprite”本来就是有魔力的、介于虚实之间的幻想角色,如火之精灵、光之精灵之类。
一旦有了这个抽象,那么无论声音还是图像,无论是软件还是硬件优化,就都要以精灵为中心了:负责游戏性循着精灵这条线更改角色位置、检测碰撞、触发相关处理逻辑;负责画面的也循着精灵这条线,更新屏幕显示、优化图形性能;负责声音的呢,同样循着精灵这条线,根据距离画面焦点的远近改变不同声源的声音大小、以及考虑是否应该添加滤波/回音等效果;负责剧情的同样可以把文字/过场动画/事件触发等绑到精灵上,设定触发条件即可,等等等等。
总之,每个人只关注自己需要关注的那个侧面,完全可以忽视其它东西的存在。这样不管游戏多么复杂、世界多么宏大,一切都可以井井有条的解决。
注意这个先后顺序:程序员设计的东西,肯定是以便利程序员自己为最优先。
先有了一个最合理最高效的架构,然后一切就可以基于这个架构提供方便。
就好像是计算机3D图形先把3D真实感图形渲染以面片为中心、然后硬件设计(显卡/GPU)才提供了迎合这个抽象的渲染体系;并不是硬件先弄出来面片,然后程序员们一阵焦头烂额手忙脚乱的往面片迁移……肯定不是这个过程,对吧。
换句话说,硬件支持可以证明一种做法的流行——流行到通过硬件管线直接支持——却几乎不可能是这种做法的来源(都出硬件支持了,可想而知这种设计在硬件出来之前有多流行)。
另一个,因为大多数人只负责自己那一块,很容易局限于“在我的领域,XX事是怎么怎么来的”。这大概就是高票二位大佬掐架的原因。
但游戏开发是个很复杂的工作,不可能迁就于某个单一方面。
比如在3D游戏逻辑开发里就没有“面片”,那是建模和渲染玩的,不会“侵染”游戏逻辑开发过程,对吧。
同样的,2D游戏开发,精灵是从软件架构来的;但这个架构在设计时,一定综合考虑了图像渲染、游戏逻辑等诸多方面,最终才决定“同时向图像组和游戏逻辑组开放sprite这个概念”——也只有站在总体架构的角度,sprite这个术语才是贴切的:如果是图像处理方单独决定的,那他们肯定不会叫它sprite。这个词和他们做的事(合成活动图片到背景)有半点关系吗?
当然,因为语言层面没有面向对象的支持,这个设计只能通过以强制类型变换为核心的各种奇技淫巧实现。
到了面向对象时代后,这套体系就沿用下来了;后来大概是为了区分吧,2D游戏仍然沿用精灵这个术语;而3D游戏就改名叫GameObject,不再使用“精灵”这个说法(但一切GameObject仍然有坐标有transform,哪怕它只是个计分牌、只关心屏幕坐标)。
正因此,当年C++等面向对象语言火爆时,游戏界反而不怎么感冒。因为这些都已经在C语言里面有了完美的解决体系,面向对象语言搞的那么一堆private啦继承啦之类约束反而会打破这个体系,把既有体系推倒重来、然后再把附着于既有体系的无数现有逻辑重写一遍,这实在太麻烦了。所以……C++,从游戏行业滚开!
嗯,当然,说“C++滚开”是开玩笑。
上古时代的确只能用C或者汇编写游戏;但后来渐渐转向C写引擎、然后通过内嵌的脚本引擎或协作其它语言写游戏逻辑这类解决方案,以便提高生产效率。那么这些协作语言完全可以用各种面向对象的新秀。
只是当时的游戏引擎被锁定到C了,推倒重来代价太大;而把和引擎配合、写游戏逻辑的语言改成C++,一个是不能像脚本那样简化和加速游戏开发任务(用脚本就图它简便,换成一辈子精通不了的C++不是找不痛快吗),一个是游戏逻辑这块并没有太高的性能需求。这等于说换C++一点好处没有,反而带来很多麻烦。因此在相当长一段时间里,最面向对象的游戏业反而普遍不用C++。
直到多年以后、基于C++开发的新游戏引擎/框架越来越成熟、越来越强大,游戏业这才慢慢抛弃C,转而拥抱C++以及其它面向对象语言。
但由于学习门槛高,C/C++系还是只用在引擎内部,游戏逻辑部分还是会选用其它语言。
上世纪70年代,在德州电器公司,一个叫Daniel Hillis的前辈最早把Sprite这个词用在计算机图形上,在展示界面上,有一些东西在实现层面并不是和整个画面融为一体的,而是『漂浮』在其他画面之上,像『幽灵』一样,所以被称为Sprite。
在2D游戏时代,Sprite非常重要,因为很多活动的角色,都是Sprite来实现的,比如《超级马里奥》中,把马里奥的所有静态形象定义好,放在一个图片里面,每次展示其中一个图片的局部(马里奥抬起左脚),然后迅速换成另一个局部(马里奥放下左脚),然后迅速换成另一个局部(马里奥抬起右脚)...... 周而复始,这样就形成了马里奥行走的动画的效果。
一般来说,所有的Sprite都放在一个图片里面,这减少了文件数,游戏读取的时候能够获得一些性能提高。
然后,在Web时代,Sprite的这个有点被网页技术也利用了,在一个网页中可能包含很多图标之类的装饰性图片,如果每个图标一个独立图片的话,需要分别下载,而且很多浏览器还有同步下载个数限制,所以,专业一点的做法,就是把多个图标放在一个图里面,利用CSS的规则让一个图标位置只显示这个图的局部。
比如,网页中的所有表情包都放在下面这张图中,显示笑脸就只显示第一行第三个那个区域就好了。
因为这种技术来源于游戏界Sprite,所以,Web界的这种技术(技巧)也就被叫做CSS Sprite。
非常赞同 @杨个毛 从硬件方面切入解答,我在此以个人拙见再做一些补充。
传统意义:Sprite是性能不足下的一种硬件图形加速手段,用于显示自由活动的二维图形块。
现代意义:Sprite是一种软件层面的抽象概念,用于表示并处理自由活动的对象。
早期的电子游戏因为显存的不足,无法完整存下一张达到游戏分辨率的图像,因此硬件上通常将屏幕划分为等大的正方形小块(Tile),小块的内容需要从另一张表中查得。这样我们就可以将画面的信息压缩为一张Tile表以及用Tile表示的图像了。
然而,游戏还需要可以动的东西,并且最好是能在屏幕内自由移动的,比如操纵的角色、敌人等。继续沿用刚才的思路,会有两种做法:一是将特定的Tile替换为这些可移动的物体。这个方案非常简单,但因为显示的单位变成了Tile,物体移动会有“跳变”的感觉。
二是实时将物体和覆盖的背景混合,作为新的Tile。这样移动的精度可以达到像素级别,但这需要消耗大量宝贵的CPU资源,且部分游戏由于成本原因使用了ROM(只读存储器),并不能写入Tile表。
因此这两种做法都并不理想。为了解决这个问题,我们需要引入新的概念:Sprite。
Sprite最早用于街机和主机游戏,且因为硬件性能的不足,需要使用外部或者内置于图形显示电路中的芯片或电路实现。出于显存不足的考虑,Sprite的表示方式与背景类似,除了Tile表外还有Sprite特有的必要信息。在显示画面的时候,由硬件将Sprite与背景实时混合。这里附上GameBoy的处理步骤作为补充资料,有能力的小伙伴可以看看(47:10起为显示过程)。
一场演讲 关于GameBoy的一切_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili从Sprite的工作流程中可以看出,这和我们的思路二很接近,只不过混合的过程交给专用硬件完成。所以在这个角度上看,Sprite可以看作两种图形混合的硬件加速手段,其中硬件处于不可或缺的地位。
顺带提一句,除了混合图形的主功能之外,部分硬件还搭载了图形变换、碰撞检测等功能,简化开发者的工作。对于不支持特定功能的硬件,则需要由软件处理这部分的逻辑,例如FC/NES游戏需要依赖CPU计算Sprite是否碰撞。
至于一部分答案提到的Sprite概念,则是在软件层面上,对这些能自由移动的对象的一种特定叫法。最大的变化在于,这种Sprite并不依赖于硬件。
实际上,由于计算机算力的高速发展,仅使用CPU进行2D图像渲染已非常常见。但为了减少不必要的重绘(比如两帧中不变的部分),开发者仍然可以采取类似的处理方式,将自行定义的Sprite绘制在特定的缓冲层,在显示的时候再将各层叠加得到最终画面。而这些Sprite,更多是为了简化开发流程而人为规定的模型和概念,与硬件不再是强相关了。
与此同时,由于Sprite更加接近面向对象编程(OOP)中的“对象”(Object),所以Sprite不再局限于绘制静态图形,而是可以自行变化内部状态(如走路时图形的改变)、与其他对象交互,以及完成一些传统Sprite不能完成的工作。
随着计算机和电子游戏的发展,Sprite的含义也在扩大。虽然传统意义上的Sprite已经逐渐退出历史的舞台,但我们仍然需要知道它的来历,以及它给电子游戏带来的变化。