|
为什么选择 Bevy 和 Rust?当然,因为Rust很受欢迎,所以我想尝试一下并学习Rust。
目前看来,Rust 使用最多的领域是数据库/区块链/跨端部署应用,这些都是传统 C++ 追求性能和多平台的领域。然而,Rust大多出现在较新的应用场景中,比如区块链。一个典型的例子。
游戏引擎主要基于C++,需要跨端部署。这是 Rust 可能进入的领域。目前(2022-7)Rust 上最著名的游戏引擎是 Bevy,一个原生 ECS 架构的游戏引擎,听起来相当新奇。众所周知,目前流行的UE、Unity等游戏引擎大多都是OOP,从一开始就没有使用Data-和ECS的方式。他们有一些历史包袱,但他们也添加了 ECS 模式。当你开始使用 Rust 时,你会发现 Rust 语言更喜欢组合而不是继承,因此 ECS 是天然的选择。
当然,现在用 Rust 写游戏的人并不多。我最喜欢的一项是()。 Rust 上著名的渲染器是 .
代码位于:
可用于在线体验:建议使用桌面访问。
游戏截图
1. 游戏基本逻辑
该游戏的灵感来自休闲 io 游戏变体,并且是 3D 的。 io游戏也更适合ECS,可能有很多更新逻辑。
游戏玩法也非常简单。你控制一个恶棍四处游荡并同化附近的路人。获得最多路人的胜利。玩家和每个敌人都被视为一个阵营。
角色可以分为两种类型
Pawn结束的条件是同一个阵营只剩下一个,并且距离另一个阵营很近。
路人有两种状态:
从未同化到同化,是根据一定范围内路人最多的阵营来判断的。
1.1 对象建模
因此,可以通过这种方式对对象进行建模,类似于UE中Actor-Pawn-的层次划分,只不过是通过组合来实现的。
<p><pre class="code-snippet__js" data-lang="properties"> <code><span class="code-snippet_outer">#[derive(Component)]</span></code><code><span class="code-snippet_outer">struct Actor {</span></code><code><span class="code-snippet_outer"> faction: i32,</span></code><code><span class="code-snippet_outer"> velocity: Vec3,</span></code><code><span class="code-snippet_outer"> accleration: Vec3,</span></code><code><span class="code-snippet_outer">}</span></code><code><span class="code-snippet_outer">#[derive(Component)]</span></code><code><span class="code-snippet_outer">struct Pawn;</span></code><code><span class="code-snippet_outer">#[derive(Component)]</span></code><code><span class="code-snippet_outer">struct PlayerController;</span></code><code><span class="code-snippet_outer">#[derive(Component)]</span></code><code><span class="code-snippet_outer">struct OpponentController;</span></code></pre></p>
<p><pre style="text-align: left;"> <code data-wrap="false"><span style="font-size: 16px;font-family: mp-quote, -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;">如果一个Entity有Actor但没有Pawn就是路人。</span>
<span style="font-family: mp-quote, -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 16px;">反之就是敌人或者玩家。</span><span style="font-family: mp-quote, -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 16px;">敌人和玩家运动</span><span style="font-family: mp-quote, -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 16px;">的方式不一样,</span>
<span style="font-family: mp-quote, -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 16px;">分别挂了OpponentConntroller和PlayerController组件。</span>
</code></pre></p>
如果Actor保存在这里,则表示其阵营,-1表示未同化状态。
还要注意的是 和 ,因为使用“力”的概念来更新运动似乎与视觉表示更一致。
1.2 设计
总共有8个
有一些行为相当于力量,根据参与者之间的关系更新参与者和底层逻辑。
有些行为是直接更新的,
一些动作传递并更新演员位置
更复杂的是,它涉及到空间查询。这里用到这个库,就用了一个。
伪代码类似:
<p><pre class="code-snippet__js" data-lang="cs"> <code><span class="code-snippet_outer">foreach actor in all actors:</span></code><code><span class="code-snippet_outer"> neighbor_actors <- find_neighbor_within_radii</span></code><code><span class="code-snippet_outer"> foreach neighbor in neighbor_actors:</span></code><code><span class="code-snippet_outer"> neighbor_faction++</span></code><code><span class="code-snippet_outer"> sort neighbor_faction</span></code><code><span class="code-snippet_outer">foreach actor in all actors:</span></code><code><span class="code-snippet_outer"> set faction by neighbor max faction</span></code></pre></p>
至于为什么需要两个循环才能解决问题,而不是一个循环?
<p><pre> <code data-wrap="false"><br />
</code></pre></p>
因为查询对象只能同时被一个作用域访问,所以空间查询时肯定会访问第一个循环,所以所有遍历到的actor只能设为只读。也是rust特殊的语法设计。
2. 贝维的使用
官方文件总比没有好。基本用法参考官方案例和一份非官方文档《Bevy Cheat Book》就比较清楚了。
2.1 输入
与Unity ECS不同的是,Bevy有更多的输入,比如这个例子
<p><pre class="code-snippet__js" data-lang="javascript"> <code><span class="code-snippet_outer">fn replay_button_system(</span></code><code><span class="code-snippet_outer"> mut interaction_query: Query<</span></code><code><span class="code-snippet_outer"> (&Interaction, &mut UiColor, &Children),</span></code><code><span class="code-snippet_outer"> (Changed<Interaction>, With<Button>),</span></code><code><span class="code-snippet_outer"> >,</span></code><code><span class="code-snippet_outer"> mut state: ResMut<State<GameState>>,</span></code><code><span class="code-snippet_outer">) {</span></code><code><span class="code-snippet_outer"> for (interaction, mut color, _) in interaction_query.iter_mut() {</span></code><code><span class="code-snippet_outer"> match *interaction {</span></code><code><span class="code-snippet_outer"> Interaction::Clicked => {</span></code><code><span class="code-snippet_outer"> state.set(GameState::Playing).unwrap();</span></code><code><span class="code-snippet_outer"> *color = PRESSED_BUTTON.into();</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer"> Interaction::Hovered => {</span></code><code><span class="code-snippet_outer"> *color = HOVERED_BUTTON.into();</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer"> Interaction::None => {</span></code><code><span class="code-snippet_outer"> *color = NORMAL_BUTTON.into();</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer">}</span></code></pre></p>
这是一个 UI 按钮。在输入中
所以实际上所有的UI都可以使用ECS来编写。
2.2 互斥查询
需要注意的一点是查询需要互斥,比如Issue #2198·/bevy()
下面的例子会报错或者进入
<p><pre class="code-snippet__js" data-lang="properties"> <code><span class="code-snippet_outer">fn ai_system(</span></code><code><span class="code-snippet_outer"> monster_query: Query<(&Monster, &Transform)>,</span></code><code><span class="code-snippet_outer"> mut hunter_query: Query<(&Hunter, &mut Speed, &Transform, &AI)>,</span></code><code><span class="code-snippet_outer">) {}</span></code></pre></p>
<p><pre> <code data-wrap="false"><br />
</code></pre></p>
因为可能有一个查询同时满足上述两个查询
需要改为
<p><pre class="code-snippet__js" data-lang="properties"> <code><span class="code-snippet_outer">fn ai_system(</span></code><code><span class="code-snippet_outer"> mut monster_query: Query<(&Monster, &mut Speed, &Transform), Without<Hunter>>,</span></code><code><span class="code-snippet_outer"> mut hunter_query: Query<(&Hunter, &mut Speed, &Transform, &AI), Without<Monster>>,</span></code><code><span class="code-snippet_outer">) {}</span></code></pre></p>
2.3 获取通过
需要注意的一点是,唯一的获取方式是通过查询,这是目前bevy0.7的一个奇怪的事情。
需要 let comp = query.get()
不。()
以上两点,再加上rust规定同一个东西只能同时使用一次,就出现了上面的问题,需要两个循环才能解决。
2.4 缺少渲染

Bevy 0.6进行了重构,可以支持大量光源。
但现在Bevy还欠缺很多:
这是什么样的 PBR?这有什么必要呢?
2.5 无编辑器
不用编辑器只写游戏逻辑还可以,但是拼UI就有点痛苦了。
如果你能快速热更新,你仍然可以不用编辑器来拼凑 UI。 Bevy没有编辑器,编译速度很慢。构建UI确实很痛苦。
如果它可以被认为是一个编辑器,那么它实际上更像是一个运行时状态查看器,而不是一个编辑器。
例如,当前的 UI 代码非常冗长。
<p><pre class="code-snippet__js" data-lang="php"> <code><span class="code-snippet_outer">commands</span></code><code><span class="code-snippet_outer">.spawn_bundle(NodeBundle {</span></code><code><span class="code-snippet_outer"> style: Style {</span></code><code><span class="code-snippet_outer"> margin: Rect::all(Val::Auto),</span></code><code><span class="code-snippet_outer"> flex_direction: FlexDirection::ColumnReverse,</span></code><code><span class="code-snippet_outer"> align_items: AlignItems::Center,</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> color: UiColor(Color::rgba(0.0, 0.0, 0.0, 0.0)),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer">})</span></code><code><span class="code-snippet_outer">.with_children(|parent| {</span></code><code><span class="code-snippet_outer"> if ordered_fac_to_count[0].0 == 0 {</span></code><code><span class="code-snippet_outer"> parent.spawn_bundle(TextBundle {</span></code><code><span class="code-snippet_outer"> style: Style {</span></code><code><span class="code-snippet_outer"> margin: Rect::all(Val::Px(20.0)),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> text: Text::with_section(</span></code><code><span class="code-snippet_outer"> "Victory!",</span></code><code><span class="code-snippet_outer"> TextStyle {</span></code><code><span class="code-snippet_outer"> font: asset_server.load("fonts/FiraMono-Medium.ttf"),</span></code><code><span class="code-snippet_outer"> font_size: 40.0,</span></code><code><span class="code-snippet_outer"> color: Color::WHITE,</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> TextAlignment {</span></code><code><span class="code-snippet_outer"> horizontal: HorizontalAlign::Center,</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> ),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> });</span></code><code><span class="code-snippet_outer"> } else {</span></code><code><span class="code-snippet_outer"> parent.spawn_bundle(TextBundle {</span></code><code><span class="code-snippet_outer"> style: Style {</span></code><code><span class="code-snippet_outer"> margin: Rect::all(Val::Px(20.0)),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> text: Text::with_section(</span></code><code><span class="code-snippet_outer"> "You Lose!",</span></code><code><span class="code-snippet_outer"> TextStyle {</span></code><code><span class="code-snippet_outer"> font: asset_server.load("fonts/FiraMono-Medium.ttf"),</span></code><code><span class="code-snippet_outer"> font_size: 40.0,</span></code><code><span class="code-snippet_outer"> color: Color::WHITE,</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> TextAlignment {</span></code><code><span class="code-snippet_outer"> horizontal: HorizontalAlign::Center,</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> ),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> });</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer"> for (fac, score) in ordered_fac_to_count {</span></code><code><span class="code-snippet_outer"> parent.spawn_bundle(TextBundle {</span></code><code><span class="code-snippet_outer"> style: Style {</span></code><code><span class="code-snippet_outer"> margin: Rect::all(Val::Px(10.0)),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> text: Text::with_section(</span></code><code><span class="code-snippet_outer"> format!("{0}: {1}\n", naming.names.get(fac as usize).unwrap(), score),</span></code><code><span class="code-snippet_outer"> TextStyle {</span></code><code><span class="code-snippet_outer"> font: asset_server.load("fonts/FiraMono-Medium.ttf"),</span></code><code><span class="code-snippet_outer"> font_size: 20.0,</span></code><code><span class="code-snippet_outer"> color: Color::WHITE,</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> TextAlignment {</span></code><code><span class="code-snippet_outer"> horizontal: HorizontalAlign::Center,</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> ),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> });</span></code><code><span class="code-snippet_outer"> }</span></code><code><span class="code-snippet_outer"> parent</span></code><code><span class="code-snippet_outer"> .spawn_bundle(ButtonBundle {</span></code><code><span class="code-snippet_outer"> style: Style {</span></code><code><span class="code-snippet_outer"> size: Size::new(Val::Px(200.0), Val::Px(65.0)),</span></code><code><span class="code-snippet_outer"> margin: Rect::all(Val::Px(20.0)),</span></code><code><span class="code-snippet_outer"> justify_content: JustifyContent::Center,</span></code><code><span class="code-snippet_outer"> align_items: AlignItems::Center,</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> color: NORMAL_BUTTON.into(),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> })</span></code><code><span class="code-snippet_outer"> .with_children(|parent| {</span></code><code><span class="code-snippet_outer"> parent.spawn_bundle(TextBundle {</span></code><code><span class="code-snippet_outer"> text: Text::with_section(</span></code><code><span class="code-snippet_outer"> "Play Again!",</span></code><code><span class="code-snippet_outer"> TextStyle {</span></code><code><span class="code-snippet_outer"> font: asset_server.load("fonts/FiraMono-Medium.ttf"),</span></code><code><span class="code-snippet_outer"> font_size: 30.0,</span></code><code><span class="code-snippet_outer"> color: Color::WHITE,</span></code><code><span class="code-snippet_outer"> },</span></code><code><span class="code-snippet_outer"> Default::default(),</span></code><code><span class="code-snippet_outer"> ),</span></code><code><span class="code-snippet_outer"> ..default()</span></code><code><span class="code-snippet_outer"> });</span></code><code><span class="code-snippet_outer"> });</span></code><code><span class="code-snippet_outer">});</span></code></pre></p>
<p><pre> <code data-wrap="false"><span style="font-size: 16px;font-family: mp-quote, -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;">如果是unity,大概率直接编辑器界面拼ui了,不用写代码,</span>
</code></pre></p>
如果用jsx来写的话,大概就是这个样子,然后用css来管理样式,显然简单多了。
<p><pre class="code-snippet__js" data-lang="xml"> <code><span class="code-snippet_outer"><div></span></code><code><span class="code-snippet_outer"> {win && <div class="title">Victory!</div></span></code><code><span class="code-snippet_outer"> {!win && <div class="title">You Lost!</div></span></code><code><span class="code-snippet_outer"> {[...Array(6)].map((x, i) =></span></code><code><span class="code-snippet_outer"> <div key={i} class="score"> {name}: {score}</div></span></code><code><span class="code-snippet_outer"> )}</span></code><code><span class="code-snippet_outer"><div></span></code></pre></p>
2.6 糟糕的网络支持
Bevy确实可以原生构建为wasm,但是它遇到了几个问题
后者指的是一个问题。具体思路是脱离Bevy,直接使用绑定到某个dom的事件来触发Bevy自己的屏幕。
3..1 速度和性能
如何比较bevy ecs和unity的性能?
作者测量了 200 x 200 x 50 ~ 2 个立方体的场景。
立方体的统一和组合场景
3.1.1 统一
值得吐槽的是,我这次用的Unity 0.51和几年前用的0.1x差别太大了,差点认不出来。
使用内置视图,
统一
3.1.2 群聚
使用内置跟踪,
<p><pre class="code-snippet__js" data-lang="nginx"> <code><span class="code-snippet_outer">cargo run --release --example many_cubes bevy/trace_chrome</span></code></pre></p>
然后使用在线工具进行可视化
群聚
3.1.3 总结
总体而言,在构建环境(Bevy=wgpu、Unity=DX11)下,Bevy的单帧时间比Unity慢十倍;
查询和计算速度几乎慢了一倍。
渲染速度慢十倍。可见Bevy的渲染性能还有很大的提升空间。
贝维
统一
总单帧时间
~400 毫秒
~40 毫秒
立方时间
〜8毫秒
〜3毫秒
更新时间
~12 毫秒
〜8毫秒
渲染时间
没有任何
〜15毫秒
渲染时间
约 350 毫秒
〜6毫秒
同样编译成llvm,unity使用burst配合ECS似乎有更好的性能。同时,由于unity的渲染批次和算法更加先进,所以一般比bevy要快很多。
从性能角度来看,Bevy并没有表现出巨大的优势。
3.2 Wasm 包大小
我们来对比一下打包后的 Wasm 的大小。下面是两个例子:
3.2.1 立方体
Unity直接使用内置的管道,
由于最近集成了unity功能,我们可以关闭很多功能。
只保留UI和渲染;关闭物理、声音、动画、WWW等各种功能。
Unity 被关闭了很多
用于编写一个旋转立方体,如下所示
统一主义
最终的 wasm 大小为 5.15MB,如果使用 gzip 压缩则为 1.85MB。
同样在Bevy中,我们也关闭了物理/声音/动画等功能,只保留UI和渲染所需的模块。
贝维瓦斯姆
最终大小,Wasm大小7.93MB,gzip压缩1.97MB。
可以说两人势均力敌。
3.2.2 许多立方体
为了渲染200万个Cube,Unity必须引入ECS系统。但ECS系统依赖很多模块
所有这些加在一起,Wasm 包体直接跃升至 19.3MB,gzip 压缩后的大小为 6.19MB。
由于原生ECS的优势,Bevy与上面的Cube没有太大区别,只是业务代码多了一些。
最终的 Wasm 大小为 8.19MB,gzip 压缩后为 2.05MB。
从这一点来看,有了ECS,Bevy的包体还是比Unity小了好几倍。
3.2.3 总结
如果你只是简单地写一个游戏,bevy 在包大小方面没有优势。只有在需要ECS的时候,bevy bag的尺寸才有数倍的优势。
然而,即使使用 Unity ECS 的 gzip 压缩,对于当前 Web 应用程序的大多数桌面应用程序来说,6.19MB 应该是可以接受的。只是手机版会比较困难。
所以从包体来看,bevy并没有表现出绝对的优势。
贝维
统一
立方瓦斯姆
7.93MB
5.15MB
立方体 Wasm Gzip
1.97MB
1.85MB
ECS 多立方体 Wasm
8.19MB
19.3MB
ECS 许多立方体 Wasm Gzip
2.05MB
6.19MB
bevy 和 unity wasm 封装尺寸对比
4 使用方便吗? 4.1 Rust好用吗?
如果你想学习 Rust,使用 bevy 可能不是一个好主意。最好实现一个链表,这样可以更好地理解生命周期和智能指针。例如
Rust 被认为是 C++ 的替代品,但显然精通 C++ 的人不会切换,现有的 C++ 应用程序也不会切换。然而,不会写C++,又想写高性能程序的人,很可能会选择Rust,并将其用在新兴领域,比如笔者。在笔者关心的领域,比如渲染的wgpu和客户端的tauri。这注定生态建设需要很长的时间。
4.2 bevy好用吗?
看来bevy在性能和构建尺寸方面没有优势。那么 bevy 在哪里有用呢?
就 wasm 而言,如果说 bevy 可能有优势的话,很可能就是它可以控制 DOM 元素并在较低级别与它们交互。 Unity在wasm上的应用场景大部分还是单体应用,比如游戏,不需要和js交互。统一定义 wasm 暴露的方法也很困难。因此,一些非游戏的3D网页商业应用可能使用rust/bevy会更方便。
但另一方面,bevy的ecs架构也在一定程度上限制了它的应用范围。如果应用程序中的所有内容都是全局唯一的,为什么我们需要查询来获取并编写逻辑?直接写逻辑不是更简单吗?而且,对于一些复杂的交互应用场景,ECS是否能够控制还有待案例验证。相反,传统的OOP方法更容易表达。因此,使用 Rust 并不一定意味着使用 Bevy。
那么贝维能做什么呢?作者对Rust本身持比较悲观的态度,看来未来几年它很可能仍然是一个玩具。但这真的很有趣。
Rust/Bevy 引擎/游戏相关项目: |
|