不喜欢 D 和 C++,程序员将 58000 行代码移植到 Jai 语言?

不喜欢 D 和 C++,程序员将 58000 行代码移植到 Jai 语言?

摘要: 将已有的上万行代码迁移至另一种编程语言, 从来就不是一件容易决定的事情, 而本文作者却信心满满地要将 5.8 万行代码全部用另一种不那么主流的语言重写, 这是为什么呢?

本文中, 我将向大家分享自己把游戏开发成果移植到 jai 语言的经历。我的游戏之前主要是用 D 和部分 C_++ 编写的, 总代码有 58620 行 (不包括库)。其实这事我已经筹划了很久, 还专门记录下了最初的期望、移植过程和最终结果。下面, 就请大家随我一道回顾这段历程。

为什么要移植

很多朋友可能好奇, 为什么会有人要大费周章把这么些代码移植成另外一种语言? 把项目做完, 之后再用新语言不好吗? 我当然有自己的考虑, 主要基于以下几点:

  • 现状让我的日常工作非常痛苦
  • 我有称手的系统移植工具, 所以移植过程应该会比较顺利
  • 对我来说, Jai 似乎比 C++ 或者 D 更有吸引力

为什么不用 C++

网上关于 C++ 缺点的讨论已经很多, 所以这里不再赘述。简而言之, C++ 几十年发展下来积累了太多错误决策, 而且永远都摆脱不掉了。它的标准库堪称灾难, 代码几乎不可能在交给他人后顺利运行。而且不知道为什么, C++ 每次新增功能都有坑。所以 C++ 的使用感受确实很差, 时时刻刻在折磨着我, 而且这个项目的发展方向在我看来也有问题。总之, 我想尽快离开 C++ 生态系统。

为什么不用 D

我的游戏开发之旅始于 2019 年, 当时我已经感受到 C++ 的问题了, 但并不知道哪种语言更好。于是我选择了 D, 理由如下: 它很像 C++, 只是去掉了不好的部分。但很遗憾, 这只是我的一厢情愿, D 在很多方面都跟 C++ 一个德行。

必须承认, D 跟 C++ 比确实有一些优势, 比如更强大的元编程、无需标头、没有未初始化的值等。但很遗憾, 这些优点根本就抵消不掉现实缺陷。

我在 Windows 上的两种 D 编译器 (dmd 和 ldc2) 之间反复横跳了四年, 最后发现至少在 Windows 平台上, D 语言的状态也就是个业余项目、往好听了说也是极不成熟的水平。很难相信这是种已经发展了 20 多年的语言。截至目前, 我不建议任何人在 Windows 上用 D 开发严肃项目, 甚至还不如继续用 C++。 从我亲身经历的问题来看, 目前最大的麻烦是 Windows 上的调试信息完全损坏:

  • 在 90% 的情况下, this 指针会缺失或出错
  • 函数堆栈上的变更经常部分 / 完全丢失或出错
  • 有时会报告变量值错误, 但却没有任何其他可见的问题
  • 静态 foreach 扩展处理不当, 令调试工具无所适从
  • minxins(相当于 D 中的宏) 生成的调试信息会令调试工具找不到正确文件 (只能按步进行反汇编)
  • 在 Visual Studio 中将指令指针移回上一行, 经常导致程序在执行下一条指令时崩溃在 D Language Code Clulb 讨论版上, 我看到 “DMD 以往曾经出过很多关于调试信息的问题”, 但号称正在处理中。可很遗憾, ldc2 也根本没好到哪去。而且除了调试之外, 元编程这边也有不少缺点和问题:
  • 不同的编译器阶段会诡异地相互影响, 导致元编程出现意外, 引发误导性错误
  • ldc2 的编译速度非常慢, 但我别无选择, 因为 dmd 有 bug
  • D 提供所谓 betterC 模式, 其中禁用垃圾收集; 但在使用此模式时, 标准库不编译而且元编程也会受到严重阻碍
  • 说明文档缺失
  • 还有其他几十个更具体的问题

总而言之: 虽然 D 在某些方面确实比 C++ 强点儿, 但其他地方的问题反而更多, 导致使用体验痛苦万分。其中最大的问题就是糟糕的调试信息和极具破坏力的垃圾收集机制。我承认, 我只是想把 D 当成改进版的 C++ 来用, 而 D 的开发者并不认同这种理解。所以 D 不适合我, 而且我现在根本就不敢信任它的调试器。

为什么选择 Jai

种种遭遇, 把我引向了 Jai。这是 Jonathan Blow 从 2014 年开始开发的一种语言, 而且直到 2019 年 12 月才向编译器敞开大门。目前封闭测试仍在进行中, 我也是约两个月前受邀体验的一员。Jai 的诞生源自 Blow 对 C++ 的失望。而且跟 D 不同, Jai 确实朝着我所认可的、能够对 C++ 做出有意义改进的方向前进。在我看来, Jai 的最大优势有二: 更快的编译速度, 还有以不受限制的编译时执行进行元编程。

值得注意的是, 这里讨论的可不是编译速度提高 20%、或者元编程功能选项略有增加之类不痛不痒的小改进。Jai 的编译速度提高了 10 到 100 倍, 而且能在编译期间执行任何操作。特别是与编译时编译器 API 相结合的元编程, 已经带来了具有深远意义的影响: 例如消除对构建系统的需求, 也摆脱了对复杂的非启发式自定义检查的依赖。除此之外, Jai 还对 C++ 做出了其他改进, 例如更好的默认值、更简单的语法、更实用的标准库、命名函数参数、上下文、using 等。这就让我有了信心, 打算通过从 D 移植到 Jai 让自己的游戏获得以下收益:

  • 将编译时间从现在的 60 秒左右降低到 5 秒以内, 最好能达到 1 秒上下
  • 调试器终于能用了
  • 用 Jai 代码替代了 build-script
  • 用元编程引入自定义编译检查, 借此捕捉更多错误
  • 用更简单的命令式代码替代复杂的元编程代码
  • 提供一系列语法改进, 减少了代码中的噪声
  • 删除了以往使用多种语言时无法避免的重复部分

为什么不考虑其他语言

的确, 我明确不想用的只有 D 和 C++, 而略感兴趣的是 Jai…… 那为什么不试试别的语言呢?

最无法回避的选项应该就是 Rust 了。它风头正劲、社区活跃, 但我还是感觉 Rust 在设计权衡方面有点问题。支持 Rust 的开发者们似乎有种 “内存安全是第一要务” 的集体心态。没错, 很多问题都源自内存安全问题, 所以我大体能够理解这样的判断。但也有很多对于安全和质量要求没那么极端的软件需求。比如, 我相信如果 C 和 C++(基本就是公认的「最不安全」语言) 能够去掉零终止字符串、默认初始化值和适当的指针 + 长度类型, 那就足以用边界检查取代 90% 的指针算法、解决临时内存管理的需求了。另外, 我觉得很多人在追求 “安全” 代码时, 其实是忽略掉了软件漏洞层出不穷的根本原因: 文化上对于复杂性的容忍, 甚至是鼓励 14。总之, 我不愿意忍受 Rust 那漫长的编译时间, 也不太认同它的文化定位。跟 Rust 不同, jai 就很关注如何控制复杂度, 这一点更符合我自己的文化判断。

还有其他一些人气稍逊的选项, 例如 Zig。我只能说它们可能也有巨大的潜力, 但我不太相信这些会是正确的选择。不是好或者不好, 只是没那么强的吸引力。

如何移植

刚开始, 我还在心里给自己鼓劲、祈祷移植过程能顺利完成。之所以比较乐观, 是因为我在游戏中设置了两套有助于移植的系统:

  • 游戏会将游戏会话的输入 (HID、加载文件、网络等) 记录到文件内并稍后重播。在重播时, 记录的输入输出确定性能让游戏循环达到完全相同的状态, 精确无误。
  • 游戏在执行过程中的各个点位上, 都会对游戏状态进行哈希处理, 并将相应的哈希值保存在不同文件当中。这样在重播时, 文件内容即可用于检查重播是否跟原始执行完全匹配。

这两项功能间有一些细微差别, 但对整体运行影响不大。依托这些功能, 我的移植计划如下:

将一小段代码由 D 或 C++ 复制到 jai, 而后编译。

调用新的 jai 代码, 替代旧有 D 或 C++ 代码。

重播录制的游戏会话。

如果重播发生分歧, 则说明移植引入了 bug, 修复此 bug。

如果重播无分歧, 则移植成功, 继续下一步。

这种方法的关键在于:

是不是所有引入的 bug 都会导致游戏状态发生显著分歧?

代码能否以较小的增量进行移植, 以便易于知晓 bug 存在、找到 bug?

第一个问题, 取决于状态哈希覆盖到多少代码。有些代码需要知晓游戏是否正在重播, 这些部分的内部行为会有所不同, 因此无法得到有意义的哈希值。例如, 写入文件功能会在重播时丢弃一切数据, 因此如果移植后的写入文件中出现了 bug, 就会被哈希过程注意到。幸运的是, 大部分代码并不属于这一类。最初的哈希在代码库中极少被用到, 但最近我开始将其扩展到插入动态数组的过程, 例如记录动态数组的大小和容量。

如此一来, 当有 bug 导致动态数组的插件会改变游戏逻辑时, 问题就会被及时注意到。因为我代码中的几乎所有功能都是靠动态数组实现的, 所以即使是在任何庞大而复杂的数学算法当中, 每一点微波的行为变化都能引起注意。

第二个问题则属于经典编程问题: 你的代码解耦程度到底有多高? 这一点非常有趣, 因为我得把代码库里的所有偶发复杂性元素找出来。首先就是模板函数: 它们无法直接移植, 因为函数定义和调用站点必须在同一编译器之内, 才能让模板正常起效——除非对模板进行手动实例化。我的代码库里有不少模板化代码, 但它们跟容器和序列化没什么紧密关联, 所以我觉得这应该不会惹出太大的麻烦。

继续推进

下面来点更直观的统计数据和图片吧。先来看我这代码库的当前状态:

总体来说, 这里有 45701 行 D 代码和 12919 行 C++ 代码, 总共 58620 行。编译时间如下:

img1

在调试模式下, ldc2 的编译过程大概需要 3 分钟, 最高占用 8 GB 内存。这时候如果打开浏览器, 那我这台 16 GB 内存的笔记本电脑就会进入满负荷运行。发布模式内存占用量更大, 为 11.5 GB。

img2

img3

如果一切顺利进行, 那在两张图中, 一切现有色块都应该会被新色块取代。能成功吗……

最后, 我想用数字来解释移植的收益, 特别是我当初的预期错得有多离谱:

我预计整个过程需要 160 个小时 (每周 40 小时, 共耗时一个月)。但我这个估计差得太多了, 很可能根本用不上一个月的时间。

我希望编译时间能从 1 分钟左右下降到最多 5 秒, 能到 1 秒上下最好。至于移植后的最终结果如何, 我将持续保持更新。

https://www.yet-another-blog.com/porting_the_game_to_jai_part0/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,作为AI语言模型,我无法直接控制计算机硬件和图形界面。但是,我可以为您提供一些思路。 要实现将显示器上画面转换成2D风格后再显示,需要进以下步骤: 1. 通过图形处理库获取当前屏幕上的图像数据。 2. 使用2D风格的转换算法对图像数据进处理。 3. 将处理后的图像数据显示在屏幕上。 在Java中,可以使用Java AWT或JavaFX中的图形处理类库获取当前屏幕截图。然后,可以通过使用Java图像处理库,如Java Advanced Imaging (JAI) 或Java ImageIO等对图像数据进处理。最后,可以使用Java GUI库将处理后的图像数据显示在屏幕上。 下面是一个简单的示例代码,演示如何使用Java AWT获取当前屏幕截图,并将其显示在JavaFX的ImageView中: ```java import java.awt.AWTException; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.Robot; import java.awt.Toolkit; import java.awt.image.BufferedImage; import javafx.application.Application; import javafx.embed.swing.SwingFXUtils; import javafx.scene.Scene; import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.stage.Stage; public class Main extends Application { @Override public void start(Stage primaryStage) throws AWTException { // 获取屏幕尺寸 Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); int width = (int) screenSize.getWidth(); int height = (int) screenSize.getHeight(); // 获取屏幕截图 Robot robot = new Robot(); BufferedImage image = robot.createScreenCapture(new Rectangle(0, 0, width, height)); // 将BufferedImage转换为JavaFX的图像格式 ImageView imageView = new ImageView(); imageView.setImage(SwingFXUtils.toFXImage(image, null)); // 创建场景 StackPane root = new StackPane(); root.getChildren().add(imageView); Scene scene = new Scene(root, width, height); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } ``` 您可以根据自己的需求,将该代码修改和扩展,实现2D风格的图像处理和显示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值