为拯救童年回忆,开发者决定采用古法编程:用Flash高清重制了一款游戏(一)

简介: 为拯救童年回忆,开发者决定采用古法编程:用Flash高清重制了一款游戏

为拯救童年回忆,开发者决定采用古法编程:用Flash高清重制了一款游戏

机器之心 2022-12-22 12:53 发表于辽宁

机器之心报道

编辑:泽南、蛋酱

实践证明,Flash 实在太糟糕了,为了重制游戏甚至要重写一个 Flash 播放器。

两年多前,Adobe 发布了一则引人关注的公告 —— 将在 2020 年 12 月 31 日终止支持 Flash,宣告了一个时代的结束。

一晃两年过去了,Adobe 早已从官方网站中删除了 Flash Player 早期版本的所有存档,并阻止基于 Flash 的内容运行。微软也已经终止对 Adobe Flash Player 的支持,并禁止其在任何 Microsoft 浏览器上运行。Adobe Flash Player 组件于 2021 年 7 月通过 Windows 更新永久删除。

当 Flash 下架之后,在世界的某个角落,这位「老同志」却仍在发挥余热。

Hapland 是 2005 年推出的一款 Flash 解谜游戏,也是很多人的童年回忆。在游戏中,玩家需要通过争取这个世界中的人们的帮助,找到打开关卡的方法,同时不要让他们被怪物吃掉或被地雷炸死。

这款游戏的图形要在 Flash 中绘制,代码要在 Flash 中编写,所有动画都在 Flash 时间轴中完成。可以这么理解:这款游戏的「骨子里都带着 Flash」。

作为游戏开发行业的一员,Robin Allen 发现,人们似乎特别喜欢 Hapland 游戏,所以他想对这款基于 Flash 的游戏的 Steam 版本进行一些修复,包括绘制更好的图形、将帧速率提高到 60FPS,并添加一些额外的「秘密」等等。


这时候该怎么做?作者详细讲述了试验过程。

一些失败的经验

失败的尝试 1:

我尝试的第一件事是让 Flash 将游戏导出为可执行文件,但失败了,因为它的性能与 2005 年一样糟糕。我想制作一个以当代帧速率运行的东西。我想摆脱 Flash Player。

失败的尝试 2:

其次,我花了太多时间摆弄 Adobe AIR(Flash 桌面 runtime)和 Starling(一个在 GPU 上绘制 Flash 场景的库)。

最后我放弃了这个,部分原因是 AIR 有很多问题而且很糟糕,也是因为我不想在一切结束时得到一个奇怪的 Adobe 结果;我想拥有自己的东西,可以做我想做的事。例如,如果我想迁移到 Linux 怎么办?

前进的道路是显然的:我必须制作自己的 Flash 播放器。

计划

以下是 Hapland 的运作方式。这里有一棵精灵树,在 Flash 中,动画精灵可以将代码附加到某些帧,当播放箭头到达那里时运行。Hapland 经常使用这一方式。游戏角色的行进路径都是很长的时间轴动画,角色经常有帧动作,比如门关了就打开,比如到了地雷区,如果还没爆炸就会触发。

时间轴中的小 “a” 是帧动作。

幸运的是,.fla 文件只是 XML。我只需要解析它,将相关数据导出为简单的自定义格式并编写一个播放器来读取它、绘制场景、处理输入并运行动画。

Hapland 仍然是一个 Flash 项目,在 Flash 编辑器中编写和维护;只有 Flash Player 会被替换。

光栅化矢量


Flash 确实支持光栅图,但实际上是为矢量图设计的。这就是 Flash 影片即使在拨号连接的情况下也能快速加载的原因。

所有 Hapland 图形都是矢量图。而 GPU 不太喜欢绘制矢量图形,却喜欢大批量的纹理三角形。所以,我需要将这些矢量光栅化。

我决定离线光栅化它们并将光栅文件打包到游戏中。在游戏运行时将它们光栅化并成为这个微小的可执行文件会很有趣,但我不想拥有那些额外的移动部件。我喜欢让尽可能多的代码在自己的开发机器上运行,这样我就可以随时关注到它。

Flash 以 XML 格式存储矢量图。你可能会说, XML 不是图形数据的一种糟糕选择,但你毕竟不是 Macromedia 的产品经理。看看这个:

在 .fla 文件中看到的矢量数据。

我不是在抱怨,这让我的工作更轻松了。

尽管我无法访问 spec,但光栅化这并不是一个难题。自 PostScript 以来,矢量图形的贝兹曲线模型无处不在。所有这些 API 的工作方式都相同。经过反复试验,我编写了一个程序来解析这些形状定义,并使用 Mac 的 CoreGraphics 库将它们呈现为 PNG。

CoreGraphics 是一个值得怀疑的选择。我选择它是因为我使用 Mac 工作,依赖性很强。但这确实成功了,所以我总是不得不在 Mac 上光栅化图形,即使是 Windows 版本也是如此。如果再一次做这件事,我可能会选择一个跨平台的库。

渲染这些 PNG 后,导出器会将它们组装成地图集?并没有,它只是按高度对所有内容进行排序,然后像文档中的文本一样逐行排列。这远非最佳,但已经足够了。

为简单起见,图集为 2048×2048 像素,这是 OpenGL 3.2 实现必须支持的最小纹理尺寸。

来自 Hapland 3 的图例集。


光栅化形状非常慢,所以为了保持合理的构建时间,我需要跳过渲染没有改变的东西。Flash 使用的压缩 XML 格式确实有每个文件的最后修改字段,但 Flash 似乎没有正确使用它们,因此您不能依赖它们。

相反,我只是对每个形状的 XML 进行哈希处理,并且只有在它发生变化时才进行重建。即使这样也失败了,因为 Flash 有时喜欢重新排列未更改的对象中的 XML 标记,但同样,这已经足够了。

用汇编程序编写二进制文件

导出器将动画数据写入自定义二进制格式。它只是逐帧通过时间轴,并写出每一帧的所有更改。

我在这里想到了写入汇编列表而不是直接写入二进制文件,我很喜欢这一点。没有 CPU 指令,只有数据,这让调试更容易,因为我可以查看汇编文件以查看生成的内容,而不是在十六进制编辑器中浏览字节。

输出.bin
















13 92 49 EC : BD 31 E8 FF09 DD BE DE : C9 5A 1D 363F C0 4E 31 : 52 FD 41 C68B 5D C0 20 : 19 1F 5F 1F54 97 8C 27 : 34 1F 30 EAA9 A9 E0 55 : 40 29 A3 1989 BC 5F 24 : 3A 98 FD B9DE 15 F2 D4 : 2A B7 41 2C4E 9D 37 D9 : E2 13 4B 0136 3F 40 08 : AC 3C FF 84E9 AE C5 2C : 11 2F 69 CF63 CE 85 D1 : A7 CB B1 1A5F 5B 60 1A : 77 99 71 B060 6E C4 C7 : 73 1F EA 1F31 0D 0C 39 : B0 86 70 42


输出.asm










; Left Sidetimeline_132:; --- Left Side, Frame 0 ---.frame_0:; --- Left Side, Frame 0, Layer 22 ---db Quaddd 0.152926, 0.162029, 0.184475, 1.000000 ; colordd 799.599976, -20.950001dd 799.599976, 556.650024dd 46.000000, 556.650024dd 46.000000, -20.950001; --- Left Side, Frame 0, Layer 21 ---; instance of shape [Left Side] [Wall Shadows] [Frame 0]dd Shapedw 1560


你更愿意调试哪个?


我本可以让导出器将字节写入一个文件,同时将单独的文本列表写入另一个文件,而不使用汇编程序,但我没有这样做,因为:

1) 汇编程序已经存在;2) 我不是必须调试它们;3) 它们支持标签。

导出器的其余部分大多不够有趣;它只是 walk the tree 并将变换矩阵、颜色效果等事物,然后继续游戏程序本身。我选择用 C++ 编写这个,因为我已经知道它,并且新事物让我害怕。

场景图

Hapland 非常适合场景图。这是 Flash 使用的模型,Hapland 就是围绕它设计的,因此尝试使用不同的模型是没有意义的。

我将场景存储在内存中,作为一棵节点树,每个节点都有一个变换,可以自行绘制并接受鼠标点击。每个具有自己行为的游戏对象都是其自己类的实例,派生自 Node.js。「面向对象」目前在游戏开发圈子里并不流行,但我使用的是 Flash,所以显然不关心这个问题。

Hapland 使用的 Flash 功能,如颜色变换和遮罩,都是存在的。不过我没有像 Flash 那样实现任意遮罩,只是实现了矩形剪辑并编辑了我所有的图形,所以所有的遮罩都是矩形。

框架脚本

几乎所有的 Hapland 逻辑都包含在附加到时间轴帧的 ActionScript 中。要如何导出所有这些东西?我可不想在我的游戏中包含 ActionScript 解释器。

一个简单的帧动作。


最后,我们使用了一些技巧,我的导出器从每一帧读取 ActionScript 并应用大量正则表达式以尝试将其转换为 C++。例如,crate.lid.play () 可能会变成 crate ()→lid ()→play ();。这两种语言在句法上非常相似,这对于许多更简单的框架动作来说效果很好,但它仍然留下了相当多的错误代码,除了手动重写所有剩余的框架动作之外别无他法。

对于 C++ 中的所有框架脚本,它们在构建时被提取并成为每个符号的 Node 子类上的方法。还会生成一个调度方法以在正确的时间调用,看起来像这样:








void tick() override { switch (currentFrame) { case 1: _frame_1(); break; case 45: _frame_45(); break; case 200: _frame_200(); break; }}


需要指出的最后一件事是脚本系统最终是某种静态类型的,这有点难受。游戏输出的最终游戏对象如下所示:









struct BigCrate: Node { BigCrateLid *lid() { return (BigCrateLid *)getChild("lid"); } BigCrateLabel *label() { return (BigCrateLabel *)getChild("label"); }
 void swingOpen() { ... } void snapShut() { ... } void burnAway() { ... }};


因此,即使一切仍然是大量的自动字符串名称查找,类型安全的单板会阻止你在错误的对象上调用错误的函数,从而使你免于在动态语言中遇到的那类烦人的 bug。

纵横比

HD 重置版游戏都会遇到画面拉伸的问题,最初的 Flash 游戏很多是页游,甚至没有全屏运行的能力,所以它们只是使用设计者喜欢的宽高比,大多是 3:2 左右。

如今最常见的纵横比似乎是 16:9,16:10 在笔记本电脑上也很流行。我希望游戏在其中任何一个方面看起来都不错,没有任何黑条或拉伸。要做到这一点的唯一方法是从原件上切掉一些部分,或者在上面添加一些部分。

所以,我为游戏画面画了两个矩形,一个比例为 16:9,另一个比例为 16:10。然后游戏根据屏幕的宽高比在它们之间进行插值,并使用插值矩形作为视图边界。只要所有重要的游戏元素都在这些矩形的交叉点内,并且它们的公共边界矩形不超出场景边缘,就可以很好地工作。

Hapland 2 的 16:10 和 16:9 框,与原来的 3:2 不同。



相关文章
|
安全 程序员 PHP
为拯救童年回忆,开发者决定采用古法编程:用Flash高清重制了一款游戏(二)
为拯救童年回忆,开发者决定采用古法编程:用Flash高清重制了一款游戏
104 0
|
数据挖掘 开发者
关于泡泡龙游戏的一点儿总结,以及分享一个好方法
游戏是一种虚拟的产品,它很难被量化,也很难像工厂流水线生产实体产品一样的去生产。因为其中涉及到的情况太多太杂,如何衡量一个游戏的体量?怎样的游戏算是大游戏,怎样的游戏算是小游戏呢?如何判断一个游戏是做完了还是没有做完呢?如何衡量一个游戏开发者的水平呢?……等等等等。这里面的每一个因素都是一个变量,这么多的无法确定的变量合在一起,想要得到一个确定的结果,很显然是不太可能的。
162 0
|
Java
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏13之英雄不要走出屏幕
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏13之英雄不要走出屏幕
145 0
|
Java
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏12之英雄自由行走
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏12之英雄自由行走
166 0
|
Java
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏16之敌人来了
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏16之敌人来了
139 0
|
Java
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏19敌人可以被打死
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏19敌人可以被打死
151 0
|
物联网 Unix 程序员
用佳能单反运行我的世界服务器,Reddit点赞37.4K,本人:专业操作,切勿模仿,相机可能变板砖
用佳能单反运行我的世界服务器,Reddit点赞37.4K,本人:专业操作,切勿模仿,相机可能变板砖
234 0
惊天“巨浪”闪现首尔!LED屏幕高清复现文艺复兴技术,网友:真·裸眼3D
惊天“巨浪”闪现首尔!LED屏幕高清复现文艺复兴技术,网友:真·裸眼3D
170 0
下一篇
DataWorks