纪元图形引擎

2020-09-28 01:18:03

图形可能有点闪烁,当东西重叠时会有很多颜色冲突,但像素填充率很高。每个物体的开销相当高,特别是对于移动的物体。你可以通过降低玩家投射的移动速度到爬行(515a:20000)并快速点击空格键来观察到这一点。在飞行中使用20多个投射的性能相当差;与标题演示(19-矩形纪元标志和8颗星)和空间区域5(1艘船和最多32颗星)的非常好的性能相比。

即便如此,移动和投射物体的成本还是比像“精英”或“星际7”这样的游戏要低,因为大纪元需要走几条捷径。

Epoch使用左手三维坐标系,查看器位于(0,0,0),右侧+X,向下+Y,远离(进入屏幕)+Z。查看器始终位于(0,0,0),以+Y向上直视Z轴,因此对象坐标始终位于眼睛(相机)空间。视图区域被视为正方形,使用45度视野的透视投影,因此位于(100,100,100)的对象将位于屏幕的右下角,位于(200,200,200)的对象也将位于屏幕的右下角。

";Far";平面位于Z=+32767,因此X/Y限制为(-32767,+32767)。任何移动到视图锥体之外的对象都将被丢弃。如果它在Z=0的位置经过近平面,则会应用播放器碰撞测试。

所有的数学,包括对象移动和透视投影,都是用简单的算术和大约4KB的查找表来完成的。

构成对象的点、线和矩形总是平行于观察者绘制的。

这款游戏使用280像素宽的全高清晰度屏幕,这超出了一个字节的容量,因此屏幕X坐标被表示为[-139,139]范围内的一个有符号的值(一个字节代表符号,另一个字节代表符号)。Y坐标简单地说是[0,191],因为屏幕本身不是方形的,所以渲染的元素宽度实际上乘以(280/192)=~1.46(假设理想投影)。

“对象”页说明了如何定义对象。最重要的是要知道,每个对象都被定义为一组元素,这些元素可以是点、水平线、垂直线或不同颜色的填充矩形。

投射物、敌舰和爆炸块都有非零的运动矢量。(明星、基地和时间门户不会自行移动。)。

如果坐标在世界空间中保持不变,玩家旋转将在最后,就在投射之前应用。因为它们总是在眼睛空间,所以当玩家旋转时,每个物体的位置和运动矢量都必须更新。

投射物以速度向量(0,0,N)直接远离玩家。如果玩家立即向上倾斜45度,投射物应该位于屏幕底部,向下并向外移动:

这显然需要更新物体的位置,使用使其保持恒定距离的旋转方程。我们还需要更新移动矢量,因为如果我们不这样做,投射物将会移动,就好像它是从玩家下方的位置发射的一样,而不是被玩家射击:

大多数情况下,球员旋转对运动的影响是不存在的或很小的,因为物体要么静止不动,要么相对缓慢地移动。主要的例外是球员投射物,它消失得足够快,以至于小错误不会被注意到。

要在游戏中看到这一点,请降低玩家的投射速度,快速射击,然后倾斜相机,使投射物位于屏幕边缘,并检查新的矢量。在一次测试中,矢量从xyz=[$00000,00000090]变为[$0000640072],虽然不是很完美(速度是151vs.144),但相当接近,而且投射物在向远处后退时看起来不会升降。

一个潜在的困难是这个游戏总是在处理移动增量。在之前的实验中,当向上倾斜时,游戏根据该帧中的操纵杆偏移量更新每一帧的向量,这与传统的世界空间方法不同,在传统的世界空间方法中,旋转是基于绝对位置和方向的。增量可能会有问题的原因是,如果数学不精确,随着时间的推移,小错误可能会累积起来。

百点外星飞船被定义为两个平面,一个在另一个后面。后平面是最后定义的,这意味着它的拉伸图位于前平面的顶部,这会造成一些擦除瑕疵。但这有一个非常奇怪的怪异之处。

如果你在对象定义(4aa 2:00 00)中禁用了飞船的向前移动,并应用主页上提到的游戏调整来创建一个只有一艘外星人飞船的空间区域,你就可以直接飞到飞船那里检查它。当你走近时,两个平面之间的Z深度差异开始暴露出坐标精度的局限性。

由于定义的形状没有公共中心点,因此平面将开始独立移动。如果你靠得很近,把十字准线移动一会儿,你就会看到这些部分开始分开:

如果你靠得很近,后部(你可以通过中间的一个闪光点来识别)实际上会开始偏离:

在其他随距离变化的游戏中,如“精英”或“星际7”,这种情况不会发生,因为位置是由物体中心的一个点决定的。Epoch将位置调整单独应用于元素坐标,只要所有点位于相同的Z深度,就可以很好地工作。在实践中,这种效果在玩游戏时并不明显,因为敌舰还没来得及让两部分明显漂移就会飞过。

纪元不存储对象的位置,所以对象移动例程只负责更新对象的移动矢量。对于敌舰来说,这是定期进行的,以使舰艇比寻的无人机更有生命力(反正它们感觉就像是无人机)。对于其他任何情况,只有当操纵杆导致视角改变时才会这样做。(编者注:这句话的意思是:“大纪元”不存储对象的位置,所以对象移动例程只负责更新对象的移动矢量。对于敌舰来说,这是周期性的,这样做比无人驾驶飞机更有生命力。)。由于前面讨论的原因,此更改必须应用于对象的平滑向量。

元素移动代码使用对象的平滑向量更新每个元素的位置(定义为左/上,也可能是右和/或下),添加玩家的向前运动,并将操纵杆角度应用于这些位置。后者不太正确,原因有两个:

更改操纵杆位置不会更改Z坐标。所以把东西从中心旋转到侧面会让它变得更远。然而,投影缩放纯粹基于Z距离,因此当物体离开屏幕中心时实际上并不会变小。

所有元素保持与屏幕平行。元素看起来应该旋转,以便在查看器旋转时朝向查看器,而不是侧向滑动。这不会让人感觉不舒服,因为这只是大纪元的视觉风格。(FWIW,Doom对设备和身体做了同样的事情。)

(详情待定;1000美元、ac00美元和b500美元的数学表需要进一步检查。此处是C++形式的表格。)。

相当简单,仅值得注意的是,我们在移动到下一个元素之前擦除并重新绘制每个元素,这意味着如果一个元素的新位置与后面元素的旧位置重叠,那么当一个对象处于运动状态时,我们将创建一些虚幻的工件。这样做对于减少不使用翻页时的闪烁很重要。在重新绘制之前擦除整个对象会增加像素变为黑色的时间,从而使屏幕刷新更有可能显示空白。

每个元素可以指定六种高分辨率颜色中的一种或两种。如果指定了两种颜色,则渲染器将按照元素中指定的速率在这两种颜色之间交替。作为优化,黑色元素会被擦除,但不会显式绘制。当元素重叠时,这可能会产生一些有趣的效果。

在高清晰度屏幕上绘制矩形的最快方法是用垂直条纹绘制。部分原因是,对于每字节7个像素(或多或少),奇数列和偶数列的位模式是不同的。通过绘制列,我们减少了切换颜色模式的次数。

STA$2000,y sta$2400,y sta$2800,y sta$2c00,y sta$3000,y sta$3400,y sta$3800,y sta$3c00,y...。

这比在循环中执行相同的操作更快,因为我们不必更新循环计数器或执行分支指令。使用要存储在A-reg中的值和Y-reg中的列调用此操作,我们可以将字节写入屏幕的速度与在6502(每个字节5个周期)上所能完成的速度一样快。

如果我们想要与现有像素混合而不是覆盖它们,我们可以这样做:

TxA或$2000,y sta$2000,y txA或$2400,y sta$2400,y txA或$2800,y sta$2800,y…

这一次,我们传递值以在X-reg中混合。为此,我们必须为每个字节花费11个周期。

这是一个写所有192行的好方法,但是如果我们想写更少的行呢?如果您设置指令,使其按从上到下的顺序修改屏幕行,则可以在用RTS覆盖最后一行之后的指令后,将JSR放入写入顶行的函数部分。当您处理完整个矩形时,您将最后一条指令恢复为其原始值。我们可以将每行开始的地址保存在查询表中。

Epoch使用此方法绘制填充矩形和垂直线。点和水平线使用不同的例程。

对于点、线和窄矩形,绘制和擦除是通过按位AND和OR操作完成的,将位与屏幕内容合并。对于至少4字节宽的矩形,我们改用STA指令。(奇怪的是,对高度没有限制,但在这个游戏中,长方形通常是宽和高一样宽的。)。STA的优势在于它的速度是STA的两倍多。STA的缺点是我们不再混合字节边缘的像素,因此相邻像素可以设置为黑色。

要查看单个元素擦除/绘制以及STA与ORA/AND的效果,查看放大标题屏幕的纪元徽标很有用。如果通过设置8d95:28和9133:28禁用STA,徽标看起来会更好:

然而,随着徽标变大,帧速率会略有下降。我们正在绘制许多相对较小的矩形,因此每个直角开销花费的周期比例相对较高,而不是像素写入,因此对此形状的效果并不太明显。(如果您直接移到友好的底座,这样它就会填满大部分屏幕,其影响会更明显。)(如果您直接移到友好的底座上,这样它就会填满屏幕的大部分区域,其影响会更明显。)(如果您直接移动到友好的底座,这样它会填满屏幕的大部分区域,其影响会更明显。)。

出现绿色条纹是因为反转用于擦除的颜色掩码的代码使用$ff而不是$7f,从而翻转高位。您可以使用8d8c:7f 8e24:7f 8e41:7f修复此问题(这可能会影响其他地方的颜色扭曲方式)。

形状本身是可以改进的。即使没有使用STA,中间的条也看起来有凹槽,因为它的形状定义不正确。你可以用48d7:80来修正这个问题。P&39;P&39;的右侧不会一直向下延伸;固定为481d:20。剩余的凹槽是擦除的结果,例如,在水平部分之前重新绘制了';P&39;/';O&39;/';C&39;中的垂直线,因此在绘制新的水平条之前,在之前的水平条被擦除的内部顶部/底部有一条缝隙。(##39;P';/';O';/';C';)。这个问题被放大了,因为水平部分足够宽,可以用STA擦除,所以它清除了矩形,并潜在地清除了左边和右边的额外像素。这种效果在屏幕上看不到,因为在屏幕边缘附近,垂直部分向左移动的速度足够快,可以超过水平部分的擦除。如果水平部分延伸到与垂直线重叠:482b:00 ff 483b:00 ff4897:00 48a7:00,我们可以降低';P&39;和';C&39;中的凹凸度:482b:00 ff 483b:00 ff4897:00 48a7:00。在第#39;E&39;和第#39;C#39;中,如果水平部分延伸到与垂直线重叠:482b:00 ff 483b:00 ff4897:00 48a7:00,我们可以减少';E&39;C';中的垂直部分。(更改元素的顺序以便最后绘制垂直部分可能就足够了,因为它们的重箭头足以用OR/AND操作绘制,并且在擦除时不会践踏水平部分。)

如果你看一下友好底座的定义,你会发现主体是一个巨大的绿色长方形,中间有三个颜色闪烁的窗口:

左边的窗户稍微宽了一点。三种闪光颜色,价格不同。左边的窗口交替橙色/黑色,中间的窗口交替紫色/黑色,右边的窗口交替白色和黑色。

紫色窗口上的白色边缘是将绿色和紫色像素相邻放置的结果,这导致相邻的位,Apple II高分辨率屏幕输出为白色。

奇怪的是,中间的窗口并没有遵循预期的模式,它不是3帧是黑色的,3帧是紫色的,而是紫色、白色、白色、绿色、黑色、黑色,实际上是紫色、白色、白色、绿色、黑色、黑色。它仍然是一个6帧的序列,但颜色有时会错。其原因与绘制和擦除矩形的方式有关,也与高分辨率屏幕上显示颜色的方式有关。

让我们检查6个连续的帧,从第一个白色帧的擦除和第二个帧的绘制开始。每一帧都是从使用STA将形状的主体绘制为绿色矩形开始的,因此我们可以指望窗口像素一开始就是绿色的。该窗口足够小,可以使用OR和AND操作进行绘制和擦除。

当前颜色为紫色。擦除窗口,将所有紫色像素AND到零。因为绿色是00101010,紫色是01010101,所以这没有效果。绘制窗口,或将所有紫色像素设置为一;这会将颜色设置为01111111。结果:白色。

当前颜色为黑色。擦除窗口,将所有紫色像素AND到零。同样,这没有任何效果。我们不绘制黑色像素,因此跳过绘制此帧。结果:绿色。

当前颜色为黑色。擦除窗口,将所有像素与零进行AND运算,因为黑色的颜色蒙版与白色的颜色蒙版相同。不要画黑色的矩形。结果:黑色。

当前颜色为紫色。擦除窗口,将所有像素AND到零。绘制窗口,将所有紫色像素取一。结果:紫色。

当前颜色为紫色。擦除窗口,将所有紫色像素AND到零,这没有任何效果。绘制窗口,或将所有紫色像素设置为一;这会将颜色设置为01111111。结果:白色。

正如您所看到的,奇怪的颜色序列是颜色被擦除和混合的方式造成的。您在左边的窗口中看不到这种情况,因为橙色和绿色占据相同的位,只是高位是否设置了不同。如果你把它改成蓝色,4ffa:50美元,它也会变得很奇怪。右侧窗口使用白/黑,因此每次都会完全擦除窗口。如果你真的靠近底部,窗口会变得足够宽,它们是用STA绘制的,所以你会得到预期的黑色/紫色颜色序列(尽管在紫色周围有黑色边框,因为STA会践踏附近的像素)。

虽然使用颜色蒙版擦除点和垂直线是有益的,但它在擦除矩形时的值是可疑的。如果一艘紫色的飞船在绿色基地前面飞行,它们的颜色会合并成白色,除非其中一艘足够大,可以使用STA(即使它们是从后到前画的)。在实践中,这样的混合事件很少见。在左右边缘使用与/或运算对于与附近的对象混合非常重要,但是使用白色蒙版擦除所有东西可以避免奇怪的行为。

这是当发射炮弹时的代码演练。代码在两个场景中进行跟踪,以显示情况是如何变化的:

常规投射速度,操纵杆完全向下和向右按压,转弯速度设置为1,前进速度设置为300。

8223:绘制状态文本、检查游戏结束条件、处理演示和时间门户飞行、检查杂键等。

862b:空格键或同时按下两个按钮。检查对象表和元素表中是否有空间。

8643:将";链接对象索引";设置为$7f,表示它链接到播放器,然后调用CreateObject创建对象类8的实例。

760f CreateObject:设置指针,生成随机数。玩家投射物的和掩码是零,所以我们总是使用形状列表中的第一个条目。

7670:确认对象/元素表中有用于该形状的空间。(射弹只有一个元素,因此上一次测试就足够了。)。抓取下一个对象槽索引,并确认它实际上是空的。将新对象添加为对象列表的头部。

7711:设置初始位置,基于形状表头+$07。玩家投射物使用$ff,并将其位置设置为(0,0,0)。这些值目前仅保存在局部变量中。

792a:完成对象初始化,复制一些附加字段并将其他字段清零。在初始Z坐标值上加上2。

79fb:执行一些特定于玩家投射物的初始化。特别地,Z向移动由调整后的前进速度增加。当速度=300时,我们增加$1E(300/10)。

7a5c:从68美元抓起下一个元素槽索引,并确认它实际上是空的。将新插槽挂接到列表中。

7a8b:将元素复制到元素表中。从形状定义中获取左/上/右/下坐标,并使用先前计算的初始位置进行偏移。玩家投射物只有一个元素,定义为矩形Z=128L=-64T=-64R=64B=64。为该对象指定初始位置xyz=(0,0,2)。元素0的最终位置:zltrb=[$0082,ffc0,ffc0,0040,0040]。

86f5:做一些关于速度、燃料和时间入口的一般事情。如果当前区域需要切换,请更新该区域。

5f66:初始化标志,检查对象寿命计数器(不用于投射物),查看对象是否活着和移动。投射物使用状态1,表示它们在移动,但不改变方向(敌舰可以驾驶)。

8c8e:元素是RECT,但是mod类型是$83,因为它还没有绘制,所以我们跳过对EraseRect的调用,直接跳到DrawElement。

922A:如果屏幕上看不到任何元素,则删除该对象。如果它越过了靠近飞机的地方,检查是否与玩家发生了碰撞。时间入口和友军基地的飞行在这里处理,与敌人物体的碰撞也在这里处理。

94eb:检查玩家投射物是否与敌舰或基地相撞。

9655:对象更新循环的底部。如果没有更多要处理的对象,请跳到主循环的顶部。