几周前,我决定为Game Boy开发一款游戏,该游戏的创作给了我极大的乐趣。 它的工作名称是Aqua和Ashes。 该游戏是开源的,并发布在
GitHub上 。
我是怎么想到这个主意的
我最近得到了一份实习职位,为我大学的网站创建了PHP和Python后端。 这是一部出色而有趣的作品,对此我深表感谢。 但是...与此同时,所有这些高级Web开发代码都使我充满了无限的欲望。 这是对使用位进行低级工作的渴望。
我每周收到有关游戏卡纸的itch.io摘要,其中宣布了
Mini Jam 4的发布。 这是一个48小时(实际上,实际上更大)的卡纸,其局限性在于以Game Boy风格创建图形。 我的第一个逻辑反应是为Game Boy创建一款自制游戏。 卡纸的主题是“季节”和“火焰”。
在考虑了可以在48小时内实现并符合主题限制的情节和机制后,我想出了一个新的解释级别的
克隆,该版本来自1993年SNES游戏Tiny Toon Adventures:Buster Busts Loose!,其中扮演Baster的玩家扮演了美式足球。
我一直喜欢这个级别的创造者如何进行一项极为复杂的运动,并摆脱了所有的花样,位置和战略元素,从而获得了一个非常有趣且轻松的游戏。 显然,对美式足球的这种简化观点将不会取代Madden,就像NBA Jam(一个类似的想法:在比常规游戏更直接的比赛方式更小得多的领域中只有4名球员)不会取代2K系列。 但是这个想法具有一定的魅力,NBA Jam的销售数字证实了这一点。
这一切与我的想法有什么关系? 我决定改成这种足球水平,使其与原始足球相似,同时又新鲜。 首先,我将比赛人数减少到只有四名球员-一名防守者和一名进攻者。 这主要是由于硬件的限制而完成的,但与此同时,它使我可以尝试使用更智能的AI进行一些试验,而不仅限于在SNES上玩游戏“左走有时跳”的原则。
为了与该主题保持一致,我将用燃烧的柱子,篝火或类似的东西(我尚未决定)以及带有火把和水桶的足球代替大门。 赢家将是控制两个篝火的团队,围绕这个简单的概念,您可以轻松地得出一个情节。 还考虑了季节:我决定每个转弯季节都会改变,以便消防队在夏天获得优势,而消防队在冬天获得优势。 这种优势看起来像是在场上的障碍物,只会干扰对方的球队。
当然,当创建两个团队时,需要两只爱而又不喜欢火的动物。 起初,我想到了火蚁和一些水虫,螳螂等,但是研究了这个问题之后,我发现冬天没有任何昆虫活跃,所以我用极地狐狸和壁虎代替了它们。 极地狐狸喜欢下雪,壁虎喜欢躺在阳光下,所以一切似乎合乎逻辑。 最后,它只是Game Boy的一款游戏。
另外,如果仍然无法理解的话,到卡纸结束时,游戏还没有接近完成。 无论如何,这仍然很有趣。
游戏男孩训练
首先,您需要确定需求。 我决定为DMG(Game Boy模型的内部名称,Dot Matrix Game的缩写)写作。 主要是为了满足游戏卡纸的要求,也因为我真的很想。 就个人而言,我从来没有玩过DMG游戏(尽管有数款Game Boy Color游戏),但是我发现2位美学对于实验来说是一个非常不错且有趣的局限性。 也许我会为SGB和CGB添加其他颜色,但是到目前为止我还没有考虑过。
我还决定使用带32K ROM +无RAM的盒带,以防万一我想创建游戏的物理副本。 CatSkull已发行了几款Game Boy游戏,例如Sheep it Up !,
并出售非常便宜的32千字节闪存盒,对我来说是完美的。 这是另一个额外的限制,但是我认为在不久的将来我不能通过这种简单的游戏克服32K的音量。 最困难的事情将是图形,如果一切都非常糟糕,那么我将尝试对其进行压缩。
至于Game Boy的工作,那么一切都很复杂。 但是,老实说,在我必须使用的所有复古式控制台中,Game Boy是最令人愉快的。 我从AssemblyDigest的作者开始撰写了
出色的教程 (至少是第一次,因为它从未完成),因此
非常不错 。 我知道最好用ASM编写,但是有时可能会很痛苦,因为硬件不是为C设计的,而且我不确定教程中提到的炫酷语言Wiz是否适合长期使用。 另外,我这样做主要是因为
我可以使用ASM。
检查提交8c0a4ea首先要做的是让Game Boy启动。 如果未在
$104
的偏移量处找到Nintendo徽标,并且标头的其余部分配置不正确,则Game Boy设备将假定墨盒插入不正确,并且将拒绝加载墨盒。 解决此问题非常简单,因为已经编写了许多有关此问题的教程。
这是我解决标题问题的方法。 没有什么值得特别注意的。
加载后执行有意义的操作将更加困难。 使系统进入无限繁忙的周期非常简单,在该周期中,它会一遍又一遍地运行一行代码。 代码执行从标签
main
开始(跳转到地址
$100
表示),因此您需要在其中插入一些简单的代码。 例如:
main: .loop: halt jr .loop
它完全不做任何事情,只是等待中断开始,然后返回到
.loop
标签。 (此后,我将省略对ASM的详细描述。如果您感到困惑,
请查看我使用的汇编器文档 。)您可能很好奇为什么我不返回
main
标签。 这是因为我希望
.loop
标签之前的所有内容都是程序的初始化,而它之后的所有内容都在每一帧发生。 因此,我不必绕过从盒式磁带加载数据并清除每一帧中的内存的循环。
让我们再迈出一步。 我使用的RGBDS汇编程序包包含一个图像转换器。 由于在此阶段我尚未为游戏绘制任何资源,因此我决定将“关于”页面中的单色按钮用作测试位图。 使用RGBGFX,我将其转换为Game Boy格式,并使用.incbin汇编器命令将其插入到
main
函数之后。
要将其显示在屏幕上,我需要以下内容:
- 液晶屏关闭
- 设置调色板
- 设置滚动位置
- 清除视频内存(VRAM)
- 将图块图形加载到VRAM中
- 下载VRAM磁贴背景图
- 再次打开液晶屏
液晶屏关闭
对于初学者来说,这成为最严重的障碍。 在第一个Game Boy上,不可能随时简单地将数据写入VRAM。 有必要等待系统什么都没画的时候。 模仿旧的CRT电视中的磷光,打开VRAM时每帧之间的间隔称为“垂直空白”或“ VBlank”(在CRT中,这是在反向扫描过程中用于消隐显像管光束的脉冲)。 (显示屏的每一行之间也有HBlank,但非常短。)但是,我们可以通过关闭LCD屏幕来解决此问题,也就是说,无论CRT屏幕的“磷迹线”位于何处,我们
都可以在VRAM中
进行记录。
如果您感到困惑,那么
此评论应该向您解释很多 。 在其中,从SNES的角度考虑了这个问题,因此不要忘记没有电子束,并且电子束的数目不同,但是在其他所有方面都非常适用。 本质上,我们需要设置FBlank标志。
但是,Game Boy的窍门是您只能在VBlank期间关闭LCD。 也就是说,我们必须等待VBlank。 为此,请使用中断。 中断是Game Boy向CPU发送硬件的信号。 如果设置了中断处理程序,则处理器将停止其工作并调用该处理程序。 Game Boy支持五个中断,其中一个中断在VBlank启动时启动。
可以用两种不同的方式处理中断。 第一个也是最常见的是
中断处理程序的任务,
其工作如上所述。 但是,我们可以通过设置该中断的启用标志并使用
di
opcode来启用特定的中断并禁用所有处理程序。 它通常不执行任何操作,但是具有退出HALT操作码的副作用,该操作码会在发生中断之前停止CPU。 (在打开处理程序时也会发生这种情况,这使我们可以退出
main
的HALT周期。)如果您感兴趣,我们还将随着时间的推移创建一个VBlank处理程序,但其中很大一部分取决于某些地址上的某些值。 由于到目前为止尚未在RAM中设置任何内容,因此尝试调用VBlank处理程序可能会导致系统崩溃。
要设置这些值,我们需要向Game Boy硬件寄存器发送命令。 有一些特殊的内存地址与设备的各个部分直接相关,在本例中为CPU,它使您可以更改其工作方式。 我们对地址
$FFFF
(中断允许位字段),
$FF0F
(已激活但未
$FF0F
位字段)和
$FF40
(LCD控制)特别感兴趣。 这些寄存器的列表可以在与Awesome Game Boy Development列表的“ Documentation”部分相关
的页面上找到。
要关闭LCD,我们仅打开VBlank中断,
$FFFF
分配值
$01
,执行HALT,直到满足条件
$FF0F == $01
,然后将地址
$FF40
的第7位分配为0。
设置调色板和滚动位置
这很容易做到。 现在液晶屏已关闭,我们无需担心VBlank。 要设置滚动位置,只需将X和Y寄存器设置为0就足够了。使用调色板,一切都变得有些棘手。 在“游戏男孩”中,您可以从4种灰色阴影(如果需要,也可以是沼泽绿色)中的任何一种的第一到第四图形分配阴影,这对于进行过渡等非常有用。 我将一个简单的渐变设置为调色板,定义为
%11100100
的位列表。
清除VRAM并加载图块图形
启动后,所有图形数据和背景图将仅由滚动的Nintendo徽标组成,该徽标在系统启动时显示。 如果打开精灵(默认情况下处于禁用状态),它们将被乱码散布在屏幕上。 您需要清除视频内存以从头开始。
为此,我需要一个类似C的
memset
的函数(我还需要一个模拟
memcpy
来复制图形数据。)
memset
函数将指定的内存片段设置为某个字节。 我自己可以轻松实现此功能,但是
AssemblyDigest教程已经具有这些功能,因此我可以使用它们。
此时,我可以通过
$00
写入
$00
清除带有
memset
的VRAM(尽管第一个提交使用的值是
$FF
,这也是合适的),然后使用
memcpy
将切片图形加载到VRAM中。 更具体地说,我需要将其复制到地址
$9000
,因为这些是仅用于背景图形的图块。 (地址
$8000-$87FF
仅用于子画面图块,地址
$8800-$8FFF
对这两种类型均通用。)
平铺地图设置
Game Boy有一个背景层,分为8x8格。 背景层本身大约需要32x32的图块,即总大小为256x256。 (作为比较:控制台屏幕的分辨率为160x144。)我需要手动逐行指示组成我的图像的图块。 幸运的是,所有图块均按顺序排列,因此我只需要用
N*11
到
N*11 + 10
值填充每行,其中
N
是行号,其余22个图块元素填充
$FF
。
打开液晶屏
在这里我们不需要等待VBlank,因为直到VBlank屏幕仍然无法打开,所以我只是再次写了LCD控制寄存器。 我还包括了背景和精灵图层,还指出了图块贴图和图块图形的正确地址。 之后,我得到了以下结果。 我还使用
ei
操作码再次打开了中断处理程序。
在这一点上,为了使其更加有趣,我为VBlank编写了一个非常简单的中断处理程序。 通过在
$40
处添加转换操作码,我可以使处理程序具有所需的任何功能。 在这种情况下,我编写了一个简单的函数来上下滚动屏幕。
这是完成的结果。 [补充:我刚刚意识到GIF循环不正确,因此必须不断传输图像。]
到目前为止,没有什么特别令人惊讶的,但是从理论上讲,我可以得到旧的Game Boy Color并查看如何在其上执行我自己的代码,这仍然很酷。
格仔床单的乐趣
为了在屏幕上绘制东西,我自然需要某种精灵。 研究了Game Boy控制台的PPU(图片处理单元)后,我决定专注于8x8或8x16子画面。 也许我需要最后一个选择,但是只是为了感觉尺寸,我迅速在方格纸上以1:8比例绘制了游戏的屏幕截图。
我想将屏幕顶部留在HUD下。 在我看来,它看起来比从下面看起来更自然,因为当它上升时,如果角色需要像Super Mario Bros中那样临时阻止HUD,他们就可以做到。 这款游戏不会有任何复杂的平台,实际上也没有关卡设计,因此我无需展示该领域的一般性看法。 屏幕上字符的位置以及可能不时出现的障碍物将足够。 因此,我负担得起相当大的精灵。
因此,如果一个正方形是一个8x8瓦片,那么无论我选择什么尺寸,一个精灵
都不足够。 考虑到游戏中几乎没有垂直运动(跳跃除外),这一点尤其正确。 因此,我决定从四个8x16子画面创建子画面。 唯一的例外是狐狸的尾巴,它占据了两个8x16的精灵。 经过简单的计算,很明显,两只狐狸和两只壁虎将占据40个精灵中的20个,也就是说,有可能添加更多的精灵。 (8x8子画面很快就会超出我的限制,这是我在开发的早期阶段不想做的。)
现在,我只需要绘制精灵。 下面是方格纸上的草图。 我有一个等待中的精灵,一个“思考”精灵,可以选择是通过还是奔跑,就像在SNES游戏中一样……就这样。 我还计划制作跑步角色,跳跃角色和对手抓住的角色的精灵。 但是对于初学者来说,我只画了等待和思考的精灵,以免复杂化。 我仍然没有做剩下的,我需要这样做。
是的,我知道,我的绘画能力不是很好。 透视是一件棘手的事情。 (是的,这只极地狐狸的脸很可怕。)但是它非常适合我。 角色设计没有任何特殊功能,但适用于游戏卡纸。 当然,我使用了真正的壁虎和极地狐狸作为参考。 难道看不到吗?
不区分。 (据记录:再次看过这些照片,我意识到壁虎和蜥蜴之间存在巨大差异。除了认为自己很愚蠢之外,我不知道该怎么做...)我想你可以猜得出索尼克(Sonic)游戏系列中的大火猫(Blaze the Cat)是狐狸的头。
最初,我希望每支球队的防守者和前锋都具有不同的性别,这样更容易区分它们。 (我也打算让玩家选择角色的性别。)但是,这需要更多的绘画。 所以我选择了雄性壁虎和雌性狐狸。
最后,我画了一个初始屏幕,因为在一张方格纸上有一个放置它的地方。
是的,动作姿势还远远不够理想。 极地狐狸应该更加沮丧和奔跑,壁虎应该看起来险恶。 Defender Fox在后台-对Doom盒子上的艺术品的有趣引用。
精灵的数字化
然后,我开始将纸制图纸转换为精灵。 为此,我使用了GraphicsGale程序,该程序最近免费提供。 (我知道您可以使用assprite,但我更喜欢GraphicsGale。)事实证明,处理sprite的工作比我预期的要复杂得多。 上面显示的精灵的这些正方形中的每个正方形在2x2网格中占用多达4个像素。 这些方块的细节往往比4像素大得多。 因此,我不得不摆脱草图的许多细节。 有时甚至很难坚持简单的形式,因为有必要留出一个眼睛或鼻子可以接受的地方。 但是在我看来,即使精灵已经完全不同,一切看起来也不错。
狐狸的眼睛失去了杏仁的形状,变成了两像素高的线。 壁虎的眼睛保持圆润。 壁虎的头部必须扩大,摆脱宽阔的肩膀,狐狸本来可以弯曲的所有弯头都被大大地抚平了。 但老实说,所有这些简单的变化并没有那么糟糕。 有时我几乎无法选择哪个版本更好。
GraphicsGale还具有用于图层和动画的便捷功能。 这意味着我可以将狐狸的尾巴与她的身体分开设置动画。 节省宝贵的VRAM空间很有帮助,因为我不需要在每一帧中重复尾部。 此外,这意味着您可以以可变的速度摇尾巴,在角色站立时放慢速度,在奔跑时加快速度。 但是,这会使编程更加复杂。 但是我仍然会承担这项任务。 我选择了4帧动画,因为这足够了。
您可能会注意到,北极狐使用三种最浅的灰色阴影,而壁虎使用三种最暗的灰色阴影。 在GameBoy上,这是可以接受的,因为尽管一个精灵中只能有三种颜色,但是控制台允许您指定两个调色板。 我做到了,所以调色板0用于狐狸,调色板1用于壁虎。所有可用的调色板都已结束,但我认为我不需要其他调色板了。
我还需要照顾好背景。 我没有理会他的草图,因为我计划将其设为纯色或简单的几何图案。 我还没有数字化屏幕保护程序,因为我没有足够的时间。
将精灵加载到游戏中
检查
be99d97提交。
保存角色图形的每个单独帧之后,便可以开始将其转换为GameBoy格式。 事实证明,在RGBDS中,有一个非常方便的实用程序RGBGFX。 您可以使用命令
rgbgfx -h -o output.bin input.png
调用它,它将创建一组与GameBoy兼容的图块。 (-h开关指定与8x16兼容的图块模式,以便从上到下而不是从左到右执行转换。)但是,当每帧都是单独的图片时,它不提供绑定,也无法跟踪重复的图块。 但是我们将把这个问题留待以后处理。
生成输出.bin文件后,只需使用
incbin "output.bin"
将它们添加到汇编器中
incbin "output.bin"
。 为了使所有内容保持一致,我创建了一个通用文件“ gfxinclude.z80”,其中包含要添加的所有图形。
但是,每次更改时手动重新生成图形都非常无聊。 因此,我编辑了build.bat文件,
for %%f in (gfx/*.png) do rgbds\rgbgfx -h -o gfx/bin/%%f.bin gfx/%%f
添加了
for %%f in (gfx/*.png) do rgbds\rgbgfx -h -o gfx/bin/%%f.bin gfx/%%f
的行
for %%f in (gfx/*.png) do rgbds\rgbgfx -h -o gfx/bin/%%f.bin gfx/%%f
,它会转换每个文件。将gfx /文件夹中的png保存到bin文件中,并将其保存到gfx / bin中。 它极大地简化了我的生活。
为了创建背景图形,我使用了一种更为懒惰的方法。 RGBASM具有dw`指令。 其后是一行0到4的8个值,等于一行像素数据。 由于背景精灵非常简单,因此复制和粘贴简单的几何图案以创建实心,条纹或棋盘状图案变得更加容易。 例如,在这里看起来像地砖。
bg_dirt: dw `00110011 dw `00000000 dw `01100110 dw `00000000 dw `11001100 dw `00000000 dw `10011001 dw `00000000
他用透视的错觉创造了一系列的条纹。 这是一种简单但明智的方法。 用草则要复杂一些。 最初,它是一组水平线,高2像素,但是我手动添加了一些像素,这些像素增加了一些噪点,使草地看起来更好:
bg_grass: dw `12121112 dw `12121212 dw `22112211 dw `11121212 dw `22112211 dw `21212121 dw `12121212 dw `12211222
图形渲染
在GameBoy中,子画面存储在称为OAM或对象属性存储器的区域中。 它仅包含属性(方向,调色板和优先级),以及图块编号。
对我来说,填充这部分内存足以在屏幕上显示精灵。虽然有小的功能。首先,您需要将ROM中的图形加载到VRAM中。 GameBoy只能渲染存储在称为VRAM的特殊内存区域中的图块。幸运的是,要从ROM复制到VRAM,足以memcpy
在程序初始化阶段执行它。原来,只有6个字符精灵和4个尾部拼贴,我已经占用了分配给精灵的VRAM区域的四分之一。 (VRAM通常分为背景和精灵区域,它们共有128个字节。)此外,仅在VBlank期间可以访问OAM。我首先说VBlank正在等待Sprite计算,但是我遇到了问题,因为Sprite计算持续了VBlank分配的所有时间,不可能完成它们。此处的解决方案是写入VBlank之外的单独存储区域,然后在VBlank期间将它们简单地复制到OAM。事实证明,GameBoy具有特殊的硬件复制过程,即一种DMA(直接内存访问,直接访问内存)。通过写入特定的寄存器并进入HiRAM的繁忙周期(因为ROM在DMA期间不可用),可以比使用该功能更快地将数据从RAM复制到OAM。memcpy
。如果有兴趣,可以在这里找到更多细节。在这个阶段,我不得不创建一个确定的程序是,同样会最终被记录在DMA。为此,我需要将对象的状态存储在其他位置。至少需要满足以下条件:- 类型(壁虎,极地狐狸或团队之一的携带物品)
- 方向
- X位置
- Y位置
- 动画框架
- 动画计时器
在第一个非常费力的决定中,我检查了对象的类型,然后根据该对象,切换到一个程序来随意绘制此类对象。例如,极地狐狸程序在X中的位置取决于方向,添加或减去16,添加两个尾部精灵,然后上下移动主精灵。这是屏幕上渲染时精灵在VRAM中的外观的屏幕截图。左侧是单个子画面,旁边是十六进制数字,从上到下-垂直和水平位置,平铺和属性标志。在右侧,您可以看到组装后的外观。对于尾部动画,它有点复杂。在第一种解决方案中,我只是简单地增加了每帧中的动画计时器,并and
使用一个值进行了逻辑增量%11
以获得帧号。然后,您可以简单地将4 *帧号(每个动画帧由4个图块组成)添加到VRAM中的第一个尾部图块,以获取存储在VRAM中的4个不同的帧。它起作用了(尤其是带有尾部磁贴搜索的尾部),但是尾部疯狂地快速摇晃,我需要找到一种降低速度的方法。在第二个更好的解决方案中,我在每帧中以及当使用它的操作值时对全局计时器执行增量and
我选择的2的阶数为0,则执行对象计时器的递增。因此,每个单独的对象都可以以所需的任何速度倒计时其动画计时器。这工作得很好,使我可以将尾巴减慢到合理的水平。难点
但是,如果一切都这么简单。不要忘了我在代码中使用所有对象自己的子过程来管理所有这一切,并且如果必须继续,则需要在每个框架中进行。我必须指定如何继续操作下一个精灵,以及它包含的图块,以手动操作寄存器。这是一个完全不稳定的系统。要绘制一帧,必须处理足够多的寄存器和CPU时间。添加对其他人员的支持几乎是不可能的,即使我成功了,系统支持也会非常痛苦。相信我,那是真正的混乱。我需要一个系统,在该系统中,用于渲染精灵的代码将是通用且直接的,因此它不会是条件,寄存器操纵和数学运算符的交织。我该如何解决?我将在本文的下一部分中讨论这一点。