跳到正文
Hacker News· gt0·· 6 天前AI 评分12

用 C 语言写一个游戏:175,000 行代码、658 个源文件的《For the Common Wealth》开发实录

Show HN: Writing a Game in C

AI 导读

开发者用纯 C99 和 SDL3 构建了 2.5D 游戏《For the Common Wealth》,约 175,000 行 C 代码、658 个源文件,无独立引擎,依赖仅 SDL3、SDL3_image 和 SDL3_mixer。全部 25 个关卡由 C# 编写的 BIABuilder 编辑器生成,导出为二进制供游戏按固定顺序读入 C 结构体,NPC 在 A* 寻路和线程池中运行“AI”。

正文 · AI 翻译

我之前写过一篇短文,讲的是我为什么选择 C。这篇是加长版,讲的是用它来构建这种规模的游戏到底是什么感觉。

它有多大?

For the Common Wealth 大约有 175,000 行 C 代码,共 658 个源文件。没有所谓的引擎。“引擎代码”和“游戏代码”之间几乎没有区分,这算是……一个选择。为我辩护一下,现有的这个“引擎”可以用于不相关的 2.5D 游戏,但它绝不是一个能复用于任何随机游戏的可复用引擎。

依赖项

SDL3,加上用于加载 PNG 的 SDL3_image 和用于声音的 SDL3_mixer。就这些。Steam 集成在运行时加载(如果 Steam 存在的话),所以同一个 exe 无论有没有它都能运行。整个游戏就是纯正的 C99。

我想把依赖项保持在绝对最低限度,主要是因为我曾设想把这款游戏发布到 Amiga 等复古平台上,但除非 SDL3 在 Amiga 上完全由 GPU 加速,否则这看起来希望渺茫,而且现在的内存占用意味着它只能是 PowerPC 版 Amiga。

内存

游戏中出现的大部分内容都存在于一次性设置好的固定大小池中:精灵、爆炸、粒子、子弹、火焰。一个关卡从这些池中申请它需要的东西,关卡重启时这些池会被重置。

关卡,以及关卡构建器(BIABuilder)

BIABuilder with mission_beach open
BIABuilder 中打开着 mission_beach。每个红框都是一个 NPC。

全部 25 个关卡都在 BIABuilder 中构建,这是我的关卡编辑器,用的是 C# 和 Avalonia,而不是 C。文件格式是 JSON,导出时 BIABuilder 会把每个关卡写成纯二进制,一个字段接一个字段,游戏以完全相同的顺序读回它的 C 结构体。没有解析器,也没有模式,只有记录末尾的魔数,这样我能很快知道导出是否出错。

BIABuilder 大约有 25,000 行 C#。

下面是同一个关卡在游戏中的样子。

mission_beach 的一分钟:穿过丛林,然后是营地。

精灵

这是一款 2.5D 游戏……这么说有点勉强。我们叫它 2.1D 游戏吧。它彻头彻尾是 2D 的,但多个精灵图层和等距美术让它看起来像 3D。大概吧。有时候吧。

精灵基于 PNG 图像。没有平铺系统,你可以用 BIABuilder 把图像放在任何地方。然后可以在 BIABuilder 中用变换、钩子和事件来编辑,在游戏中,每个精灵都会变成一个拥有 138 个成员的结构体。

主精灵图层叫做“Ordering”,每帧按 Y 基线对精灵排序,让玩家精灵能够移动到其他精灵前面和后面。

钩子

C 没有反射,所以关卡构建器不能直接按名称调用函数。取而代之的是,每个关卡用宏把自己的函数名变成字符串,然后以这些名称注册它想要暴露的函数。当 BIABuilder 说“当玩家走到这里时,调用 npc_escaped”时,游戏会在关卡加载时查找该名称并保存函数指针。游戏中大多数关卡特定的行为,比如门打开或汽车开走,都是这类钩子。这些钩子是行为和函数在 BIABuilder 中分配并在游戏中运行的主要方式。一个例子是“玩家接近”钩子。任何精灵都可以分配这个钩子,当玩家接近该精灵时,钩子会在游戏中调用。这非常适合开门之类的事情。

NPC

NPC 在“AI”线程池中运行。它们围绕位置路标巡逻,但一旦被警觉或产生怀疑,它们就会脱离巡逻,然后在 A* 寻路系统上开始寻找自己的路径。它们可以攻击、恐慌、逃跑,并互相交谈。

碰撞

碰撞有几种类型:物理,不能走过去,比如墙或洞。子弹,会挡住子弹。玩家视野,电脑控制的盟友能否看到那里。NPC 视野,NPC 能否看到那里。还有巡逻碰撞,它帮助引导 NPC 走合理的巡逻路线,也就是说,它们可以随意穿过沼泽,但不会巡逻那片沼泽。最后是仅提示碰撞,用于帐篷之类的东西。你可以直接射穿帐篷,它不会挡住子弹,但我们确实想要子弹击中布料的音效。

命名区域

这个名字很怪,来自这款游戏原本要做成温馨 RPG 的时候,那时命名区域会像“Cozy 先生的果园”之类的东西。现在它们只是任意多边形,我们对其施加效果,比如流动的水、云,或者与雨等其他特性交互。当有下雨的关卡时,我们可以为雨排斥创建命名区域,这样虽然雨可以落在关卡任何地方,但可以阻止它落在某些地方,比如室内,或墙的一侧之类。下雪同理。

运行时

启动关卡时,从磁盘加载 BIABuilder 导出内容,关卡由精灵、命名区域、NPC、位置、碰撞、任务目标、音频区域和钩子组成。钩子此时从字符串解析,在游戏过程中它们是函数指针。NPC 开始在自己的线程池中思考,动画解析为时间线,时间线是简单的结构体,可以根据当前 tick 推进动画或变换。

时间线处理游戏中几乎所有东西的移动,从行走到飞行中的子弹。飞行中的子弹是一个离散精灵,每帧检查自己的位置。它可能撞到碰撞体、玩家或 NPC,然后下一个事件开始,比如 NPC 死亡。

低语

For the Common Wealth 中的所有角色都能发出“低语”,可能是死亡时的尖叫,也可能是两个 NPC 在谈论他们家乡的女朋友。除了敌方 NPC,还有“人质”和“平民”,他们有自己的声音和动机。

我还会再做一次吗?

100% 会。

C 和 SDL3 非常适合我制作这款游戏。我在制作过程中犯了无数错误,但没有一个是由 C 造成的。我的错误来自以前从未做过游戏,边做边学。另一方面,C 一直很棒,SDL 自始至终都非常可靠。

来源:Hacker News · podule.com.au