找回密码
 立即注册
搜索
查看: 114|回复: 0

Rust 语言为何在游戏引擎领域崛起?Bevy 原生 ECS 架构解析

[复制链接]

1万

主题

0

回帖

5万

积分

管理员

积分
58009
发表于 2024-10-4 22:26:20 | 显示全部楼层 |阅读模式
为什么选择 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 引擎/游戏相关项目:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|【智道时空】 ( 京ICP备20013102号-16 )

GMT+8, 2025-5-3 22:55 , Processed in 0.100945 second(s), 20 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表