跳到主要内容

给前端同学看的跨平台游戏开发入门

· 阅读需 14 分钟
李瑾
Dora SSR 开发者。

  大家好,我是一个游戏引擎技术探索者,同时也是一名做过不少前端开发工作的程序员。如果你想知道如何从编写网页到开发游戏,那你来对地方了!

  今天我们聊聊如何使用 Dora SSR,一个支持 TSX 且跨平台在 native 运行的游戏引擎,助你轻松跨入游戏开发的世界。不必担心,说到游戏引擎并不是啥高不可攀的技术,反而和我们熟悉的前端开发工具可以有惊人相似之处。

一、游戏客户端开发也可以是一种前端开发

  首先,让我们解释一下什么是游戏引擎。简单来说,游戏引擎就是一套工具和库的集合,帮助开发者构建游戏,管理图形、声音、物理计算或碰撞检测等。对于前端开发者来说,你可以把它想象成就是一种特殊的浏览器,专门用来运行游戏。

  Dora SSR 的游戏场景管理使用了类似于 HTML DOM 的树形结构,这对我们来说再熟悉不过了。想象一下,将 div 元素换成游戏中的各种对象,CSS 动画换成游戏动画,概念也差不多,代码写法上可能也差不多,是不是觉得有点意思了?

二、从 TypeScript 到 TSX:前端技术在游戏中的应用

  许多前端开发者都熟悉 TypeScript 和 React 的 JSX 语法。在 Dora SSR 开源游戏引擎中,我们通过支持 TSX,提供了与前端开发编程模式相似的游戏开发接口。是的你没听错,就是那个 TSX。

  使用 TSX 开发游戏,意味着你可以利用已有的前端技术栈—组件、模块和其他现代前端技术,直接在游戏开发中复用这些概念。而且,Dora SSR 的性能优化确保了即使是在复杂的游戏场景中,也能保持流畅的运行。

三、挑战 100 行代码以内,教你写一个“愤怒的小鸟”like的游戏

  好了,理论知识够多了,让我们来点实际操作吧。来看看如何在 Dora SSR 中用 100 行以内的 TSX 代码编写一个类似“愤怒的小鸟”的小游戏。当然,在开始之前还是要准备开发环境,做这个事用 Dora SSR 就很简单:我有一个安装包一装,我有一个浏览器一开,嗯,开始写代码运行吧。安装启动参见:Dora 启动!

不小心装成了APK包在手机上?那就在同局域网下访问,直接在手机上进行开发调试吧

不小心装成了APK包在手机上?那就在同局域网下访问,直接在手机上进行开发调试吧

1. 编写最简单游戏场景

  在编写实际的代码之前,我们可以先写一个有特别功能的注释,它可以告诉 Dora SSR 的 Web IDE 在我们按下 Ctrl + S 保存文件时,自动热更新运行的代码,以实现代码运行结果的实时预览功能。

// @preview-file on

  然后,我们引入必要的库和组件。当然我们的代码编辑器也会提示辅助我们自动引入需要的模块,可以放到后面编码过程中再完成:

import { React, toNode, useRef } from 'DoraX';
import { Body, BodyMoveType, Ease, Label, Line, Scale, TypeName, Vec2, tolua } from 'Dora';

  在 Dora SSR 中显示一个图片很简单,只要使用<sprite>标签,最后通过toNode()函数将标签实例化为一个游戏对象就可以了。

toNode(<sprite file='Image/logo.png' scaleX={0.2} scaleY={0.2}/>);

  好的,至此你已经基本掌握大部分 Dora SSR 游戏开发的诀窍了,开始做你自己的游戏吧(认真)。

2. 编写游戏箱子组件

  接下来我们在游戏中碰撞的箱子会由 Box 组件定义,它接受 numxychildren 等属性:

interface BoxProps {
num: number;
x?: number;
y?: number;
children?: any | any[];
}

const Box = (props: BoxProps) => {
const numText = props.num.toString();
return (
<body type={BodyMoveType.Dynamic} scaleX={0} scaleY={0} x={props.x} y={props.y} tag={numText}>
<rect-fixture width={100} height={100}/>
<draw-node>
<rect-shape width={100} height={100} fillColor={0x8800ffff} borderWidth={1} borderColor={0xff00ffff}/>
</draw-node>
<label fontName='sarasa-mono-sc-regular' fontSize={40}>{numText}</label>
{props.children}
</body>
);
};

  我们使用仿 React 的函数组件的写法来完成我们箱子组件的定义,其中:

  • body 组件的 tag 属性:用于存储箱子的分数。
  • rect-fixture :定义了箱子的碰撞形状。
  • draw-node :用于绘制箱子的外观。
  • label :用于显示盒子的分数。

3. 创建 TSX 实例化后的对象引用

  使⽤ useRef 创建两个引⽤变量进行备用,分别指向⼩⻦和分数标签:

const bird = useRef<Body.Type>();
const score = useRef<Label.Type>();

4. 创建发射线

  发射线由 line 变量创建,并添加触摸(同时也是鼠标点击)的事件处理:

let start = Vec2.zero;
let delta = Vec2.zero;
const line = Line();

toNode(
<physics-world
onTapBegan={(touch) => {
start = touch.location;
line.clear();
}}
onTapMoved={(touch) => {
delta = delta.add(touch.delta);
line.set([start, start.add(delta)]);
}}
onTapEnded={() => {
if (!bird.current) return;
bird.current.velocity = delta.mul(Vec2(10, 10));
start = Vec2.zero;
delta = Vec2.zero;
line.clear();
}}
>
{/* ...在物理世界下创建其它游戏元素 ... */}
</physics-world>
);
  • onTapBegan 事件中,记录触摸开始的位置并清除发射线。
  • onTapMoved 事件中,计算触摸移动的距离并更新发射线。
  • onTapEnded 事件中,根据触摸移动的距离设置小鸟的发射速度并清除发射线。

5. 创建其它游戏元素

  接下来,我们以 <physics-world> 作为游戏场景的父级标签,在它下面继续创建游戏场景中的各个元素:

5.1 地面

  首先,我们使用 body 组件创建一个地面,并将其设置为静态刚体:

<body type={BodyMoveType.Static}>
<rect-fixture centerY={-200} width={2000} height={10}/>
<draw-node>
<rect-shape centerY={-200} width={2000} height={10} fillColor={0xfffbc400}/>
</draw-node>
</body>
  • type={BodyMoveType.Static}:表明这是一个静态刚体,不会受到物理模拟的影响。
  • rect-fixture:定义地面碰撞形状为一个矩形。
  • draw-node:用于绘制地面的外观。
  • rect-shape:绘制一个矩形,颜色为黄色。

5.2 箱子

  接下来,我们使用之前写好的 Box 组件创建 5 个箱子,并设置不同的初始位置和分数,在创建时播放出场动画:

{
[10, 20, 30, 40, 50].map((num, i) => (
<Box num={num} x={200} y={-150 + i * 100}>
<sequence>
<delay time={i * 0.2}/>
<scale time={0.3} start={0} stop={1}/>
</sequence>
</Box>
))
}
  • map 函数:用于遍历分数数组从 10 到 50,并为每个分数创建一个需要小鸟撞击的箱子。
  • Box 组件:用于创建箱子,并传入以下属性:
    • num={num}:箱子的分数,对应数组中的数字。
    • x={200}:箱子的初始 x 轴位置,为 200。
    • y={-150 + i * 100}:箱子的初始 y 轴位置,根据创建序号递增。
  • sequence 组件:用于创建要在父节点上播放的动画序列,包含以下动画:
    • delay time={i * 0.2}:延迟播放动画,延迟时间根据创建序号递增。
    • scale time={0.3} start={0} stop={1}:缩放动画,从不显示到完全显示,耗时 0.3 秒。

5.3 小鸟

  最后,我们使用 body 组件创建小鸟,并设置碰撞形状、外观和分数标签:

<body ref={bird} type={BodyMoveType.Dynamic} x={-200} y={-150} onContactStart={(other) => {
if (other.tag !== '' && score.current) {
// 累加积分
const sc = parseFloat(score.current.text) + parseFloat(other.tag);
score.current.text = sc.toString();
// 清除被撞箱子上的分数
const label = tolua.cast(other.children?.last, TypeName.Label);
if (label) label.text = '';
other.tag = '';
// 播放箱子被撞的动画
other.perform(Scale(0.2, 0.7, 1.0));
}
}}>
<disk-fixture radius={50}/>
<draw-node>
<dot-shape radius={50} color={0xffff0088}/>
</draw-node>
<label ref={score} fontName='sarasa-mono-sc-regular' fontSize={40}>0</label>
<scale time={0.4} start={0.3} stop={1.0} easing={Ease.OutBack}/>
</body>
  • ref={bird}:使用 ref 创建引用变量,方便后续操控小鸟。
  • type={BodyMoveType.Dynamic}:表明这是一个动态刚体,会受到物理模拟的影响。
  • onContactStart={(other) => {}}:小鸟的物理体接触到其它物体时触发的回调处理函数。
  • disk-fixture:定义小鸟形状为一个圆盘。
  • draw-node :用于绘制小鸟的外观。
  • label :用于显示小鸟的累积分数。
  • scale :用于播放小鸟的出场动画。

6. 完成游戏逻辑

  至此,我们已经完成了小游戏的核心逻辑。你可以根据自己的想法进一步完善游戏逻辑和增加功能。完整的 demo 代码可以见这个链接:Dora-SSR/Assets/Script/Test/Birdy.tsx。下面是一些运行效果的截图。

拖拽屏幕发射了“愤怒的小鸟”

拖拽屏幕发射了“愤怒的小鸟”

高超的技巧,使我一击获得了所有得分

高超的技巧,使我一击获得了所有得分

四、简单揭秘一下

1. 是鹿还是马

  事实上我们写的这段游戏代码,在 Dora SSR 引擎的能力下是可以确保在跨 Linux、Android、iOS、macOS 和 Windows 获得一致的运行结果。但是为了运行这段代码,我们的 Dora SSR 引擎甚至都没有做 JavaScript 运行环境的支持……(你说什么?)

  是的,Dora SSR 的底层技术实现其实是基于 Lua 和 WASM 虚拟机作为脚本语言运行环境的。对 TypeScript 的支持其实是通过整合了 TypescriptToLua(https://github.com/TypeScriptToLua/TypeScriptToLua )这个编译器提供的。TSTL 通过重新编写了 TypeScript 语言编译器的后端,将 TS 和 TSX 的代码编译为了等价运行的 Lua 代码,从而使得 TS 代码可以在 Dora 上加载运行。在 Dora 自带的 Web IDE 的代码编辑器下,可以帮助大家做 TS 的语言检查和补全以及 Dora 内置库 API 的提示。最终的使用体验,大家就可以不用管最后是鹿还是马,只要代码能通过了 TS 的编译检查,拉出来那都是一样的跑啦。

2. 和 React 有关系吗

  这个问题的答案目前是:可以有(所以截至发文前还没有)。React 最重要的能力是通过 Virtual DOM 和执行 Tree Diff 处理的过程来进行渲染组件和业务数据的状态同步,目前这个机制还没有在 Dora SSR 中实现,所以大家目前看到的用 TSX 编写出的类似 VDOM 的构建代码只会在运行时做一次性的游戏渲染对象的构建,往后都是底层 C++ 实现的引擎功能在负责继续处理。也许有一天我们会为游戏 UI 的开发,提供仿 React 通过执行 Tree Diff 做状态同步的能力,或是仿 SolidJS 基于 TSX 实现其它的渲染组件状态同步的机制。所以在这里也诚挚地邀请广大前端开发的朋友,加入我们,一起玩 Dora SSR 项目,一起研究怎么运用前端开发技术思想,为游戏开发也引入更多好用便捷的轮子吧。

给 Moonscript 重写编译器的故事

· 阅读需 13 分钟
李瑾
Dora SSR 开发者。

tokyo moon

Moonscript 是一门极为小众的编程语言

  Moonscript 是一门编译成为 Lua 代码并在 Lua 虚拟机运行的编程语言。主要语法和特性借鉴于 Coffeescript。这门语言的优势在于语言简练、具有较强表达力的同时能保留尽可能高的可读性,在表达力和可读性之间取得一个比较好的平衡点。有较为克制不那么 corner case 的语法糖。用来写一些经常变化的业务逻辑非常省力,实践下来编写相同的游戏开发类的业务逻辑,用 Moonscript 比写原生的 Lua 能缩减到 1/2,甚至到 1/3 的代码量,更少的代码对减少 Bug 的产生或是问题排查也有很多帮助。另外这门语言还有一个重要特点,据 Discord 群里的老哥说,全世界范围内的活跃用户可能只有 20 多人。还有一个更重要的特点就是这是一门 Sailor Moon Themed 的编程语言。

logo里暗藏情怀

logo里暗藏情怀

开源和免费难以为继

  Moonscript 的作者因使用这门语言开发了一些商业网站,如销售独立游戏的 itch.io,以及分享绘画作品的网站 streak.club。说为了保持这门语言的稳定性,从 2017 年开始暂缓了项目的维护,不再增加新特性,甚至 issue fix 也不积极了。当然生活不易,作者还开了 github sponsor 希望他开发的开源软件能获得更多支持。我们也没理由要求别人一直免费给大家做贡献。

不爽就自己重写

  当然,作为 Moonscript 粉丝的我对这样的状况是不能够接受的。原版 Moonscript 编译器是用 Moonscript 写的,核心是用 C 语言实现的 PEG 文法解析库解析 Moonscript 代码生成 AST 结构传到 Lua 环境中,再由 Moonscript 编译生成的 Lua 代码操作 AST 结构把Moonscript 代码翻译成 Lua 代码。这个方案还是挺浪费资源,C 语言实现的 parser 很高效,但是后续回到 Lua 环境创建大量 Lua 的数据结构,增加资源消耗和 Lua GC 时间其实并无必要,在数千行 Moonscript 代码的项目中,如果不做预编译,在运行时才动态加载 Moonscript 代码,会明显感觉到程序的长时间卡顿。另外用动态类型的语言来操作需要严格检查数据类型的 AST 结构,完全是动态语言开发的弱项。

  当然说得再多不如拿出代码有意义,所以我没有继承已有的 code base,而是直接用第二喜欢的编程语言 C++ 进行了完全的重写(第一喜欢的就是 Moonscript)。并在重写的同时顺便修复了各类作者未解决的问题,并引入一些缺失了几年的其它语言都已经用烂的编程特性。

  详见项目:Yuescript

Transpilers For Lua 和 PEG 文法

  不过说到编译生成另一门编程语言的编译器,现在更准确的叫法是叫做转译器(transpiler)。Lua 语言因为语言设计的简洁,实现了只用做一次遍历的递归下降解析器,本身的编译时间极快。又因为大家各自编程喜好的不同,很多人就打起了开发其它编程语言转译成 Lua 的转译器,扩展 Lua 语言的开发能力的想法。除了 Moonscript 外现在已有各类从 Javascript、Typescript、Lisp、C、Python、Go 和C# 等等各种语言转译成 Lua 的实现。另外也有各种给 Lua 语言加上静态类型检查的想法。

  说到底还是因为大家的审美和个性化的需求的日益增长,以及硬件的发展解放了算力,让大家都不再纠结于程序文法复杂度以及程序编译期间各种开销的问题,解放了大家研发新编程语言的生产力。就如 Python 之父曾因为历史原因,在三十年前为了确保 parser 的执行效率,降低文本解析阶段的内存消耗,实现了 LL(1) 的文法,只要一个 token 的 look ahead 就足够完成文法解析。后来算力和内存提供的条件已经大大超过以前,便开始考虑采用对程序开发更加友好的PEG文法,通过使用足够多的缓存支持无限多次的文法匹配回溯(backtrace),提升解析器开发的灵活性,以增强未来 Python 语言演化的能力。

  原版 Moonscript 也是用 PEG 文法实现的。一般实现 PEG 支持的程序库都是提供通过 parser combinator 的形式编写解析器程序。我在 C++ 中先尝试了使用 meta programming 实现的在编译期构建 parser 的黑魔法库 PEGTL,结果未获得任何开发上的增益,调试困难就不用说了,如果文法有复杂度太高,或者左递归,直接编译期提示生成函数嵌套超过最大值,左递归报错是应该,正常的嵌套太深就只能尝试调大编译参数看能不能过编译了。好不容易调好了 parser 生成一看好几个M的 binary size,才发现这个库比起应用更多的只是炫技。最终我找到了 parserlib(https://github.com/axilmar/parserlib )。运行时生成 parser,带有 AST 生成还提供一定程度的左递归文法自动解决功能,看了代码关于如何在 parse 的过程中创建 AST 的部分很精妙,就决定是它了。

用 C++ 编写 Transpiler 的优势

  有的人形容 Moonscript 是 Lua 上的一套宏系统,的确没错,很多 Moon 的语法其实就是加了能简写代码的 Lua 语法糖。Moon 转译到 Lua 只要做三步操作,第一步是解析代码生成 Moon AST,第二步是把 Moon AST 转换成对应的 Lua AST,最后一步把 Lua AST 转换成代码文本。用 C++ 操作 AST 结构的优势就是可以在编译期以及运行时以比较小的代价完成对 AST 结构的类型检查。

  并且到 C++17 版本 C++ 语言增加了很多新的编程特性,编程的表达力和抽象能力也已经变得更加强大。原版用 Moonscript 编写的 Moonscript 编译器用了近 5K 行代码,现在用 C++17 实现相同的业务功能也只用 5K 行多一点的代码量。Discord 群里另一位老哥也说他在 C++98 的年代写相同规模的项目预估代码量是不低于上万行的,C++17 已经带来了他没想到的语言进步。当然表达力、抽象力是增强了,用了一些黑魔法特性以后,生成的 binary size 也增大了很多。

  通过 C++ 的 meta programing 的能力,我们可以放心地写这样的代码:

// 检查ast节点是Exp或ExpList
if (item.is<Exp_t, ExpList_t>()) {
...
}

// 检查某节点开始是否匹配某个ast结构分支
// 并获取最后一个匹配的节点
if (auto variable =
node->getByPath<ChainValue_t, Callable_t, Variable_t>()) {
auto varName = toString(variable->name);
...
}

// 用switch语句分别处理不同的ast结构
// id作为编译期常量由编译器自动生成,无需人手工编号
switch (node->getId()) {
case id<While_t>(): {
auto while_ = static_cast<While_t*>(node);
...
break;
}
case id<For_t>(): {
auto for_ = static_cast<For_t*>(node);
...
break;
}
...
}

  通过利用模板泛型参数的功能,可以将一些参数类型的检查放到编译期。如:

node->getByPath<ChainValue_t, Callable_t>()

  就要比类似

node->getByPath("ChainValue", "Callable")

  这样的写法少很多潜在的风险,同时进行了编译期参数检查,运行时类型匹配的两重功能,动态类型的语言是很难取代这样的优势的。在这些设施的帮助下,不用额外设计特别复杂的检查机制,错误地操作 AST 结构就会产生明确的编译报错或是运行时报错,让 C++ 写 transpiler 无比爽快和省心。

方言中的方言——Yuescript 语言的生产应用

  Yuescript 在创作之初其实有一直绑定了一个开源的游戏引擎项目 Dora SSR (https://dora-ssr.net ),可以说 Yuescript 的一个重要的创作目标,就是为了让支持 Lua 语言的 Dora SSR 开源游戏引擎用上升级版的 Moonscript 语言。结合 Dora SSR 的 Web IDE,我们还给 Yuescript 语言稍微增加了一点点代码编辑器上的类型推导和代码补全的辅助能力。

  我特别喜欢在参加一些 Game Jam 活动的时候,和策划伙伴一顿头脑风暴,然后掏出 Dora SSR 引擎和 Yuescript 就是一阵不考虑太多编程设计且“不计后果”的糊玩法编码。当然编程设计也不能说是完全没有,结合 Dora SSR 游戏引擎的消息系统机制 + Yuescript 函数式风格编程的写法。Game Jam 里埋头花几个钟头写 1k 行代码左右,在一个函数内把游戏 demo 写完也是没有问题的。在 Dora SSR 的仓库 里也可以看到我们过往糊的各种 Game Jam 小游戏的 Yuescript 源码。

  所以对 Lua 的方言 Moonscript 的方言 Yuescript 语言的可用性,至少也是在 Dora SSR 项目中有过不少代码有在做验证啦。

来用 Rust 开发跨平台游戏吧

· 阅读需 17 分钟
李瑾
Dora SSR 开发者。

一、引言

  自从童年时代深陷 Warcraft III 的 MOD 魔力之中,我就一直对游戏脚本语言怀有特殊的情感。回想那时,使用暴雪开发的 JASS 语言开发魔兽争霸3的游戏关卡,尽管从今天的角度看 JASS 是极其简陋的,主要特点为静态类型 + 无 GC 功能,但它在那个尚未形成行业标准的年代,代表了对游戏开发语言的一种大胆尝试。

为什么要使用脚本语言开发游戏?

  游戏脚本语言的引入主要是为了提高开发测试的便捷性。如果直接使用 C++ 这样的底层语言,每更改一行代码,都可能需要耗费大量时间等待复杂工具链的编译与打包。而通过使用脚本语言,可以对实现游戏玩法的程序进行热加载执行,显著提升游戏的开发效率。

  随着时间的推移,如 Lua 和 JavaScript 这样的动态类型脚本语言已成为游戏开发中的常客。然而,随着编程语言的发展,我们有机会重新定义游戏脚本语言的新标准——既复古又革新,这就是 Rust + WASM 的组合。

二、Rust + WASM + Dora SSR:重新定义游戏脚本开发

  通过结合 Rust 和 WASM,我们可以在不牺牲性能的前提下,直接在例如 Android 或 iOS 设备上进行游戏热更新和测试,且无需依赖传统的应用开发工具链。此外,借助 Dora SSR 开源游戏引擎的 Web IDE 接口,使用 Rust 编写的游戏代码可以一次编译后,在多种游戏设备上进行测试和运行。

为何选择 Rust?

  Rust 提供了无与伦比的内存安全保证,而且无需垃圾收集器(GC)的介入,这使得它非常适合游戏开发,尤其是在性能敏感的场景下。结合 WASM,Rust 不仅能够提供高性能的执行效率,还能保持跨平台的一致性和安全性。

快速开始指南

  在开始开发之前,我们需要安装 Dora SSR 游戏引擎。该引擎支持多种平台,包括 Windows、Linux、macOS、iOS 和 Android。具体的安装步骤和要求,请参见官方快速开始指南:Dora SSR 快速开始

在 macOS 上运行的 Dora SSR v1.3.17 版本

在 macOS 上运行的 Dora SSR v1.3.17 版本

第一步:创建新项目

  在 Dora SSR 引擎的二进制程序启动以后,在浏览器中打开 Dora SSR 的 Web IDE,右键点击左侧游戏资源树,选择「新建」并创建名为「Hello」的新文件夹。

在浏览器中访问 Dora SSR 的 Web IDE 并新建文件夹

在浏览器中访问 Dora SSR 的 Web IDE 并新建文件夹

第二步:编写游戏代码

  然后在命令行中创建一个新的 Rust 项目:

rustup target add wasm32-wasi
cargo new hello-dora --name init
cd hello-dora
cargo add dora_ssr

  在 src/main.rs 中编写代码:

use dora_ssr::*;

fn main () {
let mut sprite = match Sprite::with_file("Image/logo.png") {
Some(sprite) => sprite,
None => return,
};
let mut sprite_clone = sprite.clone();
sprite.schedule(once(move |mut co| async move {
for i in (1..=3).rev() {
p!("{}", i);
sleep!(co, 1.0);
}
p!("Hello World");
sprite_clone.perform_def(ActionDef::sequence(&vec![
ActionDef::scale(0.1, 1.0, 0.5, EaseType::Linear),
ActionDef::scale(0.5, 0.5, 1.0, EaseType::OutBack),
]));
}));
}

  构建生成 WASM 文件:

cargo build --release --target wasm32-wasi

第三步:上传并运行游戏

  在 Dora SSR Web IDE 中,右键点击新创建的文件夹「Hello」,选择「上传」并上传编译好的 WASM 文件 init.wasm

通过 Web IDE 上传文件,相比用辅助脚本操作可能要更方便

通过 Web IDE 上传文件,相比用辅助脚本操作可能要更方便

  或者使用辅助脚本 upload.py 在 Rust 项目文件夹内上传 WASM 文件,命令如下,其中的 IP 参数为 Dora SSR 启动后显示的 Web IDE 地址,后一个参数为要上传目录的相对路径:

python3 upload.py "192.168.3.1" "Hello"

使用脚本完成一键编译、上传和开始运行

使用脚本完成一键编译、上传和开始运行

第四步:发布游戏

  在编辑器左侧游戏资源树中,右键点击刚创建的项目文件夹,选择「下载」。

  等待浏览器弹出已打包项目文件的下载提示。

三、怎么实现的

  在 Dora SSR 中实现 Rust 语言开发支持和 WASM 运行时嵌入的过程是一次新的技术探索和尝试,主要包括三个关键步骤:

1. 接口定义语言(IDL)的设计

  要在 C++ 编写的游戏引擎上嵌入 WASM 运行时并支持 Rust 语言,首先需要设计一种接口定义语言(IDL),以便于不同编程语言之间的通信和数据交换。以下是一个 Dora SSR 设计的 WASM IDL 示例,可以看出是以源语言 C++ 的程序接口为基础,增加一些转换到 Rust 接口所需要的信息的标签,比如 object,readonly,optional 等。做跨语言的接口映射其中有一个难点是 C++ 的接口设计是面向对象的,但是 Rust 并没有提供完整的面向对象设计的能力,所以一部分的面向对象的接口需要在 Rust 上额外编写代码进行功能的模拟,所幸这部分语言差异并没有特别巨大,也不用很复杂的机制设计就能解决。

object class EntityGroup @ Group
{
readonly common int count;
optional readonly common Entity* first;
optional Entity* find(function<bool(Entity* e)> func) const;
static EntityGroup* create(VecStr components);
};

  考虑到 C++ 的面向对象特性与 Rust 的设计哲学存在差异,我们在 Rust 中部分模拟了 C++ 中面向对象的行为,这需要在 Rust 中额外编写一些机制以对应 C++ 中的类和方法。这种处理方式虽然增加了一些开发工作,但保持了接口的整洁和系统的可维护性。

2. 生成胶水代码的程序

  第二步是编写一个程序,通过 IDL 生成 C++、WASM 和 Rust 之间互相调用的胶水代码。为了实现这一点,我们选择使用 Dora SSR 项目自创的 Yuescript 语言。Yuescript 是基于 Lua 的一门动态编程语言,它结合了 Lua 语言生态中的 lpeg 语法解析库来处理 IDL 的解析和胶水代码的生成。使用 Yuescript 的好处是它继承了 Lua 的灵活性和轻量级,同时提供了更丰富的语法和功能,适合处理复杂的代码生成任务。以下是使用 PEG 文法编写的 IDL 解析器的代码节选。

Param = P {
"Param"
Param: V"Func" * White * Name / mark"callback" + Type * White * Name / mark"variable"
Func: Ct P"function<" * White * Type * White * Ct P"(" * White * (V"Param" * (White * P"," * White * V"Param")^0 * White)^-1 * P")" * White * P">"
}

Method = Docs * Ct(White * MethodLabel) * White * Type * White * (C(P"operator==") + Name) * White * (P"@" * White * Name + Cc false) * White * Ct(P"(" * White * (Param * (White * P"," * White * Param)^0 * White)^-1 * P")") * White * C(P"const")^-1 * White * P";" / mark"method"

3. 嵌入 WASM 运行时和代码整合

  最后一步是在游戏引擎中嵌入 WASM 运行时以及所生成的 C++ 胶水代码,完成代码的整合。对于 WASM 运行时,我们选择使用 WASM3,这是一个高性能、轻量级的 WebAssembly 解释器,它支持多种 CPU 架构,能够简化编译链的复杂性,并提高跨平台的兼容性。通过这种方式,Dora SSR 能够支持在各种架构的硬件设备上运行 Rust 开发的游戏,极大地提高了游戏项目的可访问性和灵活性。

  在整合过程中,我们发布了供 Rust 开发者使用的 crate 包,包含所有必要的接口和工具,以便开发者未来可以轻松地基于 Dora SSR 游戏引擎开发和再发布使用 Rust 语言编写的其它游戏模块。

四、性能比较

  Dora SSR 游戏引擎同时也提供了 Lua 脚本语言的支持。目前使用的是 Lua 5.5 版本的虚拟机,和 WASM3 也是一样的没有做 JIT 的实时机器码的生成而只是在虚拟机中解释执行脚本代码。所以我们可以为这两个相近的脚本方案做一些性能比较。

  在比较之前,我们可以大概判断,不考虑 Lua 语言执行 GC 的耗时,因为 Lua 语言本身的动态特性,C++ 映射到 Lua 的程序接口往往得在运行时做接口传入参数类型的实时检查,另外 Lua 对象的成员属性的访问查找也需要在运行时通过一个 hash 结构的表进行查找,这些都是静态类型的 Rust 语言 + WASM 虚拟机不需要付出的开销,或者只用付出更小的开销的场景。以下是一些基础的性能测试的案例,专门选取了 C++ 端没有做太多计算处理的接口,来比较跨语言调用传参的性能差异。

  • Rust 测试代码
let mut root = Node::new();
let node = Node::new();

let start = App::get_elapsed_time();
for _ in 0..10000 {
root.set_transform_target(&node);
}
p!("object passing time: {} ms", (App::get_elapsed_time() - start) * 1000.0);

let start = App::get_elapsed_time();
for _ in 0..10000 {
root.set_x(0.0);
}
p!("number passing time: {} ms", (App::get_elapsed_time() - start) * 1000.0);

let start = App::get_elapsed_time();
for _ in 0..10000 {
root.set_tag("Tag name");
}
p!("string passing time: {} ms", (App::get_elapsed_time() - start) * 1000.0);
  • Lua 测试代码
local root = Node()
local node = Node()

local start = App.elapsedTime
for i = 1, 10000 do
root.transformTarget = node
end
print("object passing time: " .. tostring((App.elapsedTime - start) * 1000) .. " ms")

start = App.elapsedTime
for i = 1, 10000 do
root.x = 0
end
print("number passing time: " .. tostring((App.elapsedTime - start) * 1000) .. " ms")

start = App.elapsedTime
for i = 1, 10000 do
root.tag = "Tag name"
end
print("string passing time: " .. tostring((App.elapsedTime - start) * 1000) .. " ms")

运行结果

Rust + WASM:
object passing time: 0.6279945373535156 ms
number passing time: 0.5879402160644531 ms
string passing time: 3.543853759765625 ms

Lua:
object passing time: 6.7338943481445 ms
number passing time: 2.687931060791 ms
string passing time: 4.2259693145752 ms

  可以看出,除了字符串类型的接口传参调用外,在 Dora SSR 中实现的其它类型的接口的 Lua 跨语言调用性能要比 WASM 跨语言调用几乎慢一个数量级。字符串类型的接口推断是因为性能消耗大头主要都是在字符串对象的拷贝上,跨语言调用的开销远比内存拷贝的开销小,所以结果差距不大。

五、用户体验之谈

  在游戏开发中引入 Rust 语言,我个人体验到了与传统所不同的生产力提升,特别是在与大型语言模型(如 ChatGPT)进行代码生成辅助方面。与传统的 C 或 C++ 相比,Rust 的严格编译器为游戏开发提供了一个更加稳固和安全的编程环境。

  比如使用大语言模型辅助编码时,在生成 C 或 C++ 甚至很多动态类型的语言时,尽管很多时候生成的代码可以通过编译,但在运行时往往仍隐藏着许多难以察觉的错误和缺陷。这些问题可能包括内存泄漏、指针或是引用误用等等,这些都是游戏开发中常见且难以调试的问题。然而,在 Rust 中,许多这类问题都可以在编译阶段被有效捕捉并修正,这得益于 Rust 的所有权和借用机制,以及其在类型安全和内存安全方面的设计优势。

  通过在 Dora SSR 游戏引擎中引入对 Rust 的支持,我发现编写游戏脚本不仅更加安全,也更加高效。这使得游戏开发不再是一个错误排查的过程,而是一个更加专注于创造和实现想象中游戏的过程。Rust 的这些优势,加上 WASM 的跨平台能力,极大地扩展了我们的游戏开发能力和可能性。

六、结语

  选择 Dora SSR + Rust 作为游戏开发工具不仅是追求技术的前沿,也是对游戏开发流程的一次新的探索。在这里诚邀每一位热爱游戏开发的朋友加入我们的社区,一同探索这一激动人心的技术旅程。欢迎访问我们的 GitHub 仓库 来了解更多信息,并参与到我们的开发中来。一起开创游戏开发的新纪元吧!

从编译器、游戏引擎到游戏掌机——我是这样做独立游戏的

· 阅读需 11 分钟
李瑾
Dora SSR 开发者。

引言

  自己开发制作游戏是一个儿时起就有的梦,特别是长时间接触魔兽争霸3世界编辑器后,我对游戏引擎和开发工具也有着特别的兴趣。学生时代接触编程以后,梦的外延开始扩散,不满足于使用各式编程语言做开发,开始维护一门自己喜欢的写游戏业务逻辑的编程语言 Yuescript,因为学习图形学和作为学习项目重写 Cocos2d-x 有了 Dora SSR 游戏引擎。工作后因为对游戏掌机的喜爱,开始与伙伴合作研发自由开放的可编程游戏掌机设备——吉祥机,实现自己游戏梦终极的 Digital Freedom。

游戏脚本语言的乐趣与挑战

编程语言游乐场!

编程语言游乐场!

  各式新的编程语言的学习是充满乐趣,对不同语言工具的接触也会带来不同的编程理念和程序设计思想。对于复杂多变的游戏玩法的脚本编程(Scripting)我也形成了自己编程偏好,即使用一门尽可能简洁和表达力强的编程语言来编写容易变化的业务逻辑,可以转译为 Lua 语言执行的 Yuescript 就是满足这个需求的产物。后来随着使用自己的 Dora SSR 游戏引擎项目有了更多的游戏开发体验,又为 Dora SSR 游戏引擎引入了 Teal(为 Lua 语言添加静态类型检查能力的语言),Typescript(进一步增强代码编辑器提示和代码检查的语言),JSX 和 XML(提供描述性代码进行组件化开发的语言)等等。每一种脚本语言都能在特定的游戏开发场景发挥优势,并通过转译到最终运行的同样的Lua语言进行无缝的互通调用。不只是基于 Lua 语言的扩展,Dora SSR 游戏引擎还在尝试通过 WASM 虚拟机来支持更加多样的可以用做游戏脚本编程的语言,如 Rust 和准备支持的 C++ 和 Go 等,兼顾性能与引擎的运行时扩展能力。

游戏引擎的创新之路

随时随地用任何设备制作游戏!

随时随地用任何设备制作游戏!

  说到游戏引擎大家总是想到高性能高质量的图形渲染,搭建复杂的游戏场景。实际上作为独立游戏开发者,或是游戏制作的爱好者,并不是人人都有条件追求3A游戏的制作(钞能力)。我认为很多 2D 游戏或是 2D 混合 3D 效果的游戏也能表达展现十分有创意和独特的游戏作品。而且能运行自己制作游戏的终端最好是不受限制的,再进一步,也许能用于开发游戏的终端也可以是不受限制的。所以就有了 Dora SSR 游戏引擎的项目目标,在尽可能多的设备上为游戏开发爱好者提供便捷易用的环境甚至是游戏开发 IDE。一直以来游戏开发这件也成为了我的个人生活体验的一部分。哪怕只有碎片化的时间和手边随机可用作游戏开发和运行的设备,我也想有空就利用起来碎片化地写两行游戏代码,或是调试一个游戏功能,并把它变成了一种比较随性惬意的休闲活动。

  所以 Dora SSR 搭建了通过游戏引擎运行时内置用于游戏开发的 Web IDE 服务器,可以通过其它方便做输入的设备通过 Web 浏览器做访问,并实现直接在任意的终端运行设备上直接编写运行和调试游戏开发的代码。同时获得代码编辑器可视提示服务、以及使用其它游戏开发和资源管理的可视化工具。目前 Dora SSR 在努力之下已具备了在 Windows、macOS、iOS、Android、多个 Linux 发行版上进行游戏开发的能力。

向着自由开放的游戏掌机梦想迈进

开源开放?软件和硬件全都要!

开源开放?软件和硬件全都要!

  到此我觉得对游戏开发能力的自由和开放体验的追求还远不到尽头。作为喜好各式掌机的老玩家,在体验了诸多国产开源掌机的商业产品后,我感觉深深的不满足。用掌上游戏机玩游戏目前还是在卷硬件参数和外观设计来提供体验的差别,而我期待的掌机并不只是玩游戏上的体验,还应该是一个可以用来自由的开发、运行甚至发行自制游戏的设备。很多掌机厂商都有自己的商业化模式和获得盈利的闭环,所以不会允许硬件设备获得太多可编程定制的能力。于是和同样对硬件发烧的伙伴一起研究构建完全自由开放的掌机设备。并尽可能提供包括机器的计算核心、外设和外观均可进行模块化的定制和更换的能力(使科技不再以换壳为本)于是又有了“吉祥机”的项目。

吉祥机 + Dora SSR 游戏引擎

吉祥机 + Dora SSR 游戏引擎

回到制作游戏的初心

社区在做的开源独立游戏项目《灵数奇缘》

社区在做的开源独立游戏项目《灵数奇缘》

  所以折腾了半天我的游戏到底做出来了没有呢?答案当然是做了,但没完全做出来啦。在生成式 AI 大模型进入彻底火爆前夕的2020年我们就想象了一个关于未来的 AI 的游戏故事,人的物质需求已经得到完全满足,生下来的目的只剩下了通过进行游戏娱乐,并通过采集过程数据给AI生产有创造力和展现智能的训练数据。人生的价值都是由未来的银行通过评估人的游戏活动所创造的智能数据的质量和价值,来进行货币分配而评定的。人类贡献的数据训练出的 AI 会帮助人完成一切的物质生产、到人类个体的养育和社会管理的工作。在这样的背景下,人还会有什么样的故事。最后游戏输出的价值观就是人生来就应该是改造世界的主体,而不是只会适应一切现状的被改造的客体。也呼应了我和我的伙伴们一直在追寻的东西,想要不被与生而来的一切所定义,就去靠自己的主动创造去重新定义一切。

  如果对我们在做的编程语言、游戏引擎、游戏掌机或是游戏项目感兴趣,欢迎 Star 我们的仓库或是进入我们的 Q 群聊聊。目前的项目都还在比较早起的阶段,但是会互相整合和验证迭代,关注我们也可以及时看到项目是怎么做的以及我们的进展。

  最后我们诚挚邀请每一位对游戏开发有热情的朋友加入我们,无论是贡献代码、提供意见还是分享我们的项目,您的每一份努力都能帮助我们共同实现游戏开发自由的梦想。

项目地址

🌈 Dora SSR - 从《绿野仙踪》到游戏世界的奇幻之旅

· 阅读需 3 分钟
李瑾
Dora SSR 开发者。

🌟


  每个伟大的创造背后,往往隐藏着一个魔法般的故事。Dora SSR的灵感正是源自我童年时代最钟爱的故事之一——《绿野仙踪》。在这个经典故事中,Dorothy与她的小狗Toto,伴随着他们的新朋友——心灵缺失的铁皮人、缺乏智慧的稻草人和胆怯的狮子,共同踏上了一段既充满挑战又奇妙的冒险之旅。


  Dora SSR引擎的旅程,正如Dorothy在《绿野仙踪》中的经历一般,是一次关于探索与发现的旅程。它不仅是一个技术产品,更是一个激发创意、实现梦想的平台。我们期望每一位使用Dora SSR的开发者,都能像Dorothy及其伙伴们那样,在游戏开发的征途中勇敢追梦,克服困难,发掘自我。

🚀


  在Dora SSR的世界中,我们鼓励开发者们像稻草人一样追求智慧、像铁皮人一样探寻心灵之爱、像狮子一样寻找勇气。每一款游戏都代表着一次全新的冒险,每一个创意都是对未知世界的一次探索。我们坚信,通过这个平台,每个人都能找到他们心中的“翡翠城”——梦想的实现之地。

🌍


  Dora SSR的力量,正如Dorothy及其伙伴们一样,源自于其不断壮大的社区——一个充满创意、支持和协作的环境。我们的社区就是我们的“绿野仙踪”,在这里,每个人都能找到自己的声音和位置。

🔥


  我们的旅程才刚刚开始。Dora SSR将持续成长和发展,如同《绿野仙踪》中的角色们一样,不断地学习、进步和超越。我们期待在这个平台上见证更多的创意和奇迹的诞生。

🤝


  因此,让我们一起携手努力,在Dora SSR的奇幻旅程中续写我们自己的“绿野仙踪”故事!在这段旅程中,每个人都可以成为Dorothy,每个创意都有潜力成为下一个奇迹!