Crowded Plaza, a game with Bevy/Rust| 拥挤广场:使用Bevy/Rust开发小游戏

为什么是Bevy和Rust? 当然因为Rust有点火,笔者想试试玩玩,学习学习Rust。

目前看上去Rust应用比较多的领域是数据库/区块链/跨端部署应用,也就是传统C++的领域,追求性能和多平台,但是Rust多出现在较新的应用场景,比如区块链就是个典型的例子。

游戏引擎主要还都是C++的,并且需要跨端部署,这个领域也是Rust有可能进入的。Rust上游戏引擎目前(2022-7)最有名的就是Bevy了,一个原生ECS架构的游戏引擎,听着还挺新奇的。众所周知,目前流行的游戏引擎如UE,Unity大多是OOP,并没有从一开始就使用Data-Oriented和ECS的方式,有一些历史包袱,但也都新加了ECS模式。入门Rust就会发现,Rust语言比较推崇组合而不是继承,ECS也就顺理成章。

当然现在用Rust写游戏的还不多,笔者比较喜欢的Embark Studio就是其中之一,Embark (github.com) 。Rust上著名的Kajiya渲染器便是Embark的作品。

代码在 maajor/crowded-plaza: An experimental game with bevy (github.com)

可玩版本在 https://crowded-plaza.vercel.app

1. 游戏基本逻辑

这个游戏受到CrowdedCity的启发,是一个休闲io游戏的变种,并且是3D的。io游戏还比较适合ECS,其中可能有大量entity更新的逻辑。
这个玩法也很简单,控制一个小人游走,同化附近的路人,获取最多的路人就可以获胜。玩家和每个敌人算一个阵营,
角色可以分两种

  • pawn,是每个敌人或者玩家控制的。玩家通过鼠标控制,敌人就随机游走
  • actor,是路人

Pawn结束的条件是同阵营只剩自己且接近了别的阵营。
路人有两个状态:

  • 未同化状态,随机游走
  • 同化状态,跟随角色,这个行为比较像flocking,有align,aggregate,repulse几个基础行为。

从未同化到同化状态,判定依据是周边一定范围内最多的路人的阵营。

1.1 对象建模

所以对象可以这么建模,比较类似UE的Actor-Pawn-Controller的层次划分,只不过是通过组合实现的。

如果一个Entity有Actor但没有Pawn就是路人,反之就是敌人或者玩家。

敌人和玩家运动的方式不一样,分别挂了OpponentConntroller和PlayerController组件。

Actor这里存了faction就是所在阵营,-1即未同化状态。

同时还记了velocity和acceleration,因为用“力”的概念来更新运动看上去更符合视觉表现。

1.2 System设计

总共有8个system

有一些行为等效于作用力,根据actor之间相互关系,更新actor的acceleration和底层逻辑

  • change_actor_faction_system,判断路人的阵营转换,更换视觉表现,
  • follow_pawn_system,同阵营跟随pawn,更新actor的acceleration
  • repulse_actor_system,actor之间相互排斥,更新actor的acceleration

有些行为直接更新velocity

  • change_direction_player_system,通过鼠标交互控制玩家的运动,更新velocity
  • change_direction_actor_system,未同化的路人随机游走,更新velocity
  • change_direction_opponent_system,敌人随机游走,更新velocity

有些行为通过velocity和acceleration更新actor位置

  • move_actor_system,实际运动actor,更新位置,这里会acceleration
  • move_pawn_system,实际运动pawn,更新位置,这个不考虑acceleration

比较复杂的是change_actor_faction_system,会涉及空间查询,这里使用了bevy_spatial这个库,用了一个kdtree。伪码类似:

至于,为什么要两个loop解决,不能一个loop,

因为,query对象同时只能被一个作用域borrow,第一个loop里在空间查询时一定会被borrow,就只能让所有遍历到的actor只读了。也是rust一个特殊的语法设计。

2. Bevy使用

官方文档聊胜于无,看用法基本参考官方案例bevy/examples at main · bevyengine/bevy (github.com)

和一个非官方的文档Introduction – Unofficial Bevy Cheat Book (bevy-cheatbook.github.io) 还是比较清楚的

2.1 System的输入

和Unity ECS不同的是,Bevy的System输入有更多中,比如这个例子中

它是一个system,但是是一个UI按钮的system。system的输入里,

  • 可以有Resource,即一些全局资源,比如这里用的游戏状态,也可以定义用户定义自己的全局状态
  • 可以是Component,component可以通过 With, Without, Changed修饰,甚至交互状态interaction都是个Component

2.2 互斥查询

一个注意的点是,system中query需要互斥,比如Failed to access two mut components in one system with two querys · Issue #2198 · bevyengine/bevy (github.com)

下面这个例子会报错create disjoint Queries or merging conflicting Queries into a ParamSet

因为有可能有一个entity,同时满足上面两个Query需要改成

2.3 通过Entity获取Component

一个注意的点是,通过entity获取component只有通过query一种方式,这时bevy0.7目前比较奇怪的地方。

需要let comp = query.get(entity)

没有entity.get_component<>()

上面这两点,再加上rust规定同一个同时只能被borrow一次,就出现了上面change_actor_faction_system中,需要两个loop解决。

2.4 渲染Feature缺少

bevy 0.6中重构了RenderGraph和Clustered Forward, 可以支持大量光源 Bevy – Bevy 0.6 (bevyengine.org)不过现在bevy还有很多欠缺:

  • 不支持ibl,hdri图
  • 没有SSAO/HBAO/GTAO任何一种
  • 没有后处理
  • 没有PCF/PCSS/EVSM任何一种软阴影
  • 没有烘焙
  • culling很差

这算什么PBR,这么点feature需要RenderGraph干嘛?

2.5 没有编辑器

光写游戏逻辑没有编辑器还好,但是为了拼UI是有点痛苦。

如果像javascript可以快速热更,没有编辑器拼ui还凑活。在Bevy里没有编辑器编译又慢,拼ui真的太痛苦了。

如果说bevy_inspector_egui也算编辑器吧,其实更像是运行时的状态查看器,不能算authoring的编辑器。比如目前的一个UI代码,十分啰嗦。

如果是unity,大概率直接编辑器界面拼ui了,不用写代码,

如果用jsx来写大概会这样,然后用css管理样式,明显简单很多。

2.6 Web支持

Bevy确实可以原生build到wasm,不过遇到几个问题

  • Bevy渲染模块wgpu的webgl2后端在移动端支持不好,打开无法操作
  • bevy原生打出来的wasm不能全屏canvas

后者参考了一个github issue https://github.com/mvlabat/bevy_egui/issues/56, 具体思路就是脱离bevy,直接用web_sys绑定一个dom window的onresize事件,触发bevy自己的resize屏幕。

3. Benchmark

3.1 速度与性能

用bevy ecs和unity entities比较性能如何呢?笔者测了一个 200 x 200 x 50 ~ 2 million cube的场景,

bevy中2M cube
unity中2M cube

3.1.1 Unity

值得吐槽的是,笔者这次使用的Unity Entities 0.51,和几年前使用的0.1x差别太大了,都快不认识了。

使用自带的Profiler查看,

3.1.2 Bevy

使用自带的trace,cargo run --release --example many_cubes bevy/trace_chrome

然后用在线工具可视化 https://ui.perfetto.dev/

3.1.3 总结

总的看下来,Native环境下,(Bevy = wgpu vulkan, Unity = DX11 )单帧时间Bevy比Unity慢了十倍;

其中System的查询和计算慢了接近一倍渲染慢了十倍。

可以看出Bevy的渲染性能还有很大提升的空间。


BevyUnity
总单帧时间~400 ms ~40 ms
Rotate Cube时间~8 ms~3 ms
更新Transform时间~12 ms~8 ms
渲染Batching时间~15 ms
渲染时间~350 ms~6 ms

同样是编译到llvm,unity使用burst compiler搭配ECS看上去有更好的性能。同时由于unity的渲染batch和culling算法更为先进,总体上比bevy快不少。

从性能上看,Bevy没有展现出巨大的优势。

3.2 Wasm包体大小

我们来对比下打包Wasm的大小,这里有两个例子:

  • 一个只有一个Cube的极小场景
  • 一个有2M个Cube使用ECS的场景

3.2.1 Minimal Cube

Unity直接使用built-in管线,

得益于最近unity功能也package化,我们可以关掉很多功能,

只保留UI和渲染;关闭Terrain,物理,声音,动画,WWW,AssetBundle等各种功能。

unity里关掉很多功能

使用Monobehavior写一个旋转的cube,长这样

unity minimal wasm

最终wasm大小5.15MB,如果用gzip压缩是1.85MB。
同样Bevy我们也关掉物理/声音/动画等功能,只保留UI和渲染需要的 bevy_core_pipeline, bevy_renderbevy_pbrbevy_text, bevy_uibevy_sprite 模块。

不能关掉ECS,因为它原生ECS。

bevy minimal wasm

最终尺寸,Wasm大小7.93MB,gzip压缩1.97MB。
可以说二者半斤八两。

3.2.2 Many Cube

渲染两百万个Cube,Unity就必须引入ECS系统了。但是ECS系统依赖于很多模块

  • Hybrid Renderer&SRP,DOTS的渲染系统依赖于一个特殊的SRP
  • Job/Burst/Mathematic等Entities模块必须依赖的组件

这些全加进来,Wasm包体直接飙到19.3MB,gzip压缩尺寸6.19MB。

而Bevy由于原生ECS的优势,和上面的Minimal Cube差距不大,只是多了些业务代码。最后Wasm大小8.19MB,gzip压缩2.05MB。

这样来看,带着ECS的话bevy的包体比unity还是小了几倍的。

3.2.3 总结

单纯写一个游戏的话,从包体尺寸上看bevy也没啥优势。只有在必须ECS的情况下,bevy包体尺寸才有几倍的优势。

不过就算是unity ecs这个gzip的压缩,6.19MB对于当前的网页应用来说,应该是大部分桌面端的情况都可以接受的。只是在移动端的情况可能比较麻烦。

所以从包体上看,bevy没有展现出绝对的优势。


BevyUnity
Minimal Cube Wasm Uncompressed7.93 MB5.15 MB
Minimal Cube Wasm Gzip1.97 MB1.85 MB
ECS Many Cube Wasm Uncompressed8.19 MB19.3 MB
ECS Many Cube Wasm Gzip2.05 MB6.19 MB

4 好用吗?

rust好用吗?

如果为了学Rust,使用bevy可能不是个好主意。不如去实现个链表,更能理解生命周期和智能指针。 Introduction – Learning Rust With Entirely Too Many Linked Lists (rust-unofficial.github.io)

Rust被认为是C++的替代品,但很明显熟练C++的人不会转过来,已有的C++应用也不会转过来。不过,不会写C++的人想写高性能程序,大概率会选择Rust,在新兴领域使用它。

比如在笔者关注的领域,如渲染的wgpu,客户端的tauri。这注定了它会需要一定漫长的生态建设时间。

bevy好用吗?

看上去bevy从性能和build尺寸上,都没什么优势。那么bevy在什么地方会有用呢?

在wasm这方面,如果说bevy可能有个优势,大概就是可以更底层地控制DOM元素,以及与webapp交互了。Unity在wasm上的大多数的应用场景还是单一app,比如游戏,不需要和js交互。也较难在unity里定义wasm暴露的方法。因此非游戏类的一些3D web商业应用或许用rust/bevy会方便。

但另一方面,bevy的ecs架构一定程度上局限了它的应用范围。如果app里全是全局唯一的component,那何必需要query获取并把逻辑写进system?直接把逻辑写进component不是更容易?况且对于一些复杂交互的应用场景,ECS能否驾驭还有待案例验证,反而是OOP的传统方式更容易表示。所以,用rust也不一定用bevy。

所以bevy能干嘛?笔者相对于Rust本身持比较悲观的态度,目前看在未来几年大概率还是个玩具。不过确实挺好玩的。

Reference

Rust的游戏相关项目

Rust/Bevy的学习资料

欢迎关注微信公众号:码工图形

2 thoughts on “Crowded Plaza, a game with Bevy/Rust| 拥挤广场:使用Bevy/Rust开发小游戏

  1. momo说道:

    关于性能测试 使用得是同一图像后端吗
    测试方式建议编译成release 使用Intel GPA等三方工具来对比

    1. maajor说道:

      后端不一样,vulkan vs DX11,但我感觉batching等优化方式不一样,renderpass设计就不一样,不是shader执行效率的问题,用GPA也看不出啥。

发表评论

邮箱地址不会被公开。 必填项已用*标注