三年全职 Rust 游戏开发,真要放弃 Rust 吗?
延伸学习
hecs 里预留实体 ID 的机制类似于对象池模式;reserve_entities 和 reserve_entity 函数提供了一种机制来生成实体 ID,这可以视为一种延迟的工厂模式;
Thunderdome 库中 Index 结构体:
代码语言:javascript
复制
/// Index type for [`Arena`] that has a generation attached to it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Index {
pub(crate) slot: u32,
pub(crate) generation: Generation,
}
slot (槽位)用于索引内部的数组,而 generation (世代)用于验证引用的有效性。当一个元素被移除后,其 slot 可能被新的元素重用,但是新元素会有一个递增的 generation号,以此来避免旧引用意外访问新数据。
代码语言:javascript
复制
pub fn get2_mut(&mut self, index1: Index, index2: Index) -> (Option<&mut T>, Option<&mut T>) {
// 首先检查两个索引是否相同,如果相同则抛出 panic,因为不能从同一个资源获取两个可变引用
if index1 == index2 {
panic!("Arena::get2_mut is called with two identical indices");
}
// 处理索引指向相同槽位但属于不同世代的情况
if index1.slot == index2.slot {
// 借用检查器的限制使我们必须两次访问存储以获取正确的返回值
// 如果第一个索引有效,则返回第一个元素的可变引用和 None
if self.get(index1).is_some() {
return (self.get_mut(index1), None);
} else {
// 如果第一个索引无效,则返回 None 和第二个元素的可变引用
return (None, self.get_mut(index2));
}
}
// 如果索引指向不同的槽位,则可以安全地分割存储来获取两个独立的可变引用
let (entry1, entry2) = if index1.slot > index2.slot {
// 如果 index1 的槽位大于 index2,则先分割这部分,确保每部分分别被独立借用
let (slice1, slice2) = self.storage.split_at_mut(index1.slot as usize);
(slice2.get_mut(0), slice1.get_mut(index2.slot as usize))
} else {
// 如果 index2 的槽位大于 index1,则先分割这部分
let (slice1, slice2) = self.storage.split_at_mut(index2.slot as usize);
(slice1.get_mut(index1.slot as usize), slice2.get_mut(0))
};
// 通过索引的世代号来获取每个槽位的有效值
// 只有当索引中的世代号与存储中对应槽位的世代号匹配时,引用才被视为有效
(
entry1.and_then(|e| e.get_value_mut(index1.generation)),
entry2.and_then(|e| e.get_value_mut(index2.generation)),
)
}
这个方法的主要步骤是:
- 检查索引是否相同
- 处理相同 slot 但不同 generation 的索引
- 分割存储来安全获取引用
- 通过 generation 号验证实体的有效性
此方法中的设计模式主要是:
- 安全分割:通过
split_at_mut安全地分割存储区,从而允许同时独立地访问两部分数据。 - 条件验证:通过验证索引的
generation号来确保数据的有效性和一致性。
所以本质上还是没有违反 Rust 借用检查规则,真正能返回两个可变借用的情况只存在:两个给定的索引指向不同槽位,并且这两个索引都有效时。
“ ECS 解决了错误类型的问题”
作者说,由于 Rust 的类型系统和借用检查器的工作方式,ECS 成为了“我们如何让东西引用其他东西”的问题的自然解决方案。
但其实,ECS 架构并非 Rust 独创。早在多年前,暴雪《守望先锋》就使用了 ECS 架构[7]。只不过 Rust 的类型系统和借用检查器特别适合实现这种架构,所以在 Rust 生态中比较流行 ECS 架构。
ECS(Entity Component System)是一种常用于游戏开发和高性能计算应用的架构模式,它通过将数据(组件)和行为(系统)从实体中分离出来,使得数据处理更为高效、灵活。
在传统的面向对象编程中,对象间常常通过引用或指针相互关联,这会引入复杂的生命周期管理问题和潜在的内存安全风险。ECS 通过以下方式简化了这些问题:
- 组件存储:在 ECS 中,组件是独立存储的,并且通常不直接引用其他组件。相反,它们可能包含指向其他实体或组件的标识符(如实体 ID)。这种方法避免了直接引用,简化了生命周期管理,因为组件的添加和删除是独立处理的。
- 实体和组件的解耦:实体在 ECS 中通常作为一个轻量级的标识符存在,它本身并不持有数据。所有的数据都是通过组件来表示的,这些组件被组件管理器以一种高效的方式存储和处理。这种解耦确保了在实体生命周期结束时,可以简单地清理其所有组件,而不用担心传统意义上复杂的对象图清理问题。
- 系统的独立操作:每个系统都独立操作一组特定的组件,这样的设计减少了对共享数据的需求,降低了复杂度和出错的可能。系统间通信通常通过共享的资源或通过事件来进行,这些机制都可以在 Rust 的安全模型下高效实现。
作者也认同 ECS 架构的优势。他的重点是,他认为社区把 ECS 滥用了。
作者列举了三个问题:
- 具有实际指针的指针型数据。问题很简单,如果字符 A 跟随字符 B,而 B 被删除(并被释放),那么指针将无效。
Rc<RefCell<T>>结合弱指针。虽然这样可能可行,但在游戏中性能很重要,由于内存局部性,这些的开销是非常大的。- 对实体数组进行索引。在第一种情况下,我们会得到一个无效的指针,而在这种情况下,如果我们有一个索引并且移除一个元素,索引可能仍然有效,但指向其他东西。
这些问题,作者用前面提到的 Thunderdome 库解决了,并且他非常推荐这个库。
但是他认为,社区里大多数人认为 ECS 的好处实际上是 generational arena (Thunderdome)的好处。当人们说“ECS 给我带来了很好的内存局部性”,但他们只查询类似于 Query<Mob, Transform, Health, Weapon> 的移动对象时,他们实际上做的基本上相当于 Arena<Mob> :
代码语言:javascript
复制
struct Mob {
typ: MobType,
transform: Transform,
health: Health,
weapon: Weapon
}
所以,作者认为,有的时候你要分清自己可能仅仅需要一个 generational arena ,而非 ECS 。很多时候人们使用 ECS 是因为它解决了“我应该把我的对象放在哪里”的特定问题,而不是真正使用它进行组合,也不真正需要它的性能。这没有错,只是当这些人最终在互联网上与其他人争论,试图说服其他人他们的做事方式是错误的,并且他们应该按照上述原因使用 ECS 的某种方式,而实际上他们一开始并不需要它时,就会出现问题。
欢迎咨询,九影专注软件定制15年,提供:游戏开发、元宇宙、线上展厅、虚拟仿真、AR/VR、小程序/公众号、网站/App、全景展示/3D建模、CRM/OA/ERP系统定制等。
热线:021-50309719 电话/微信15000568602

微信“扫一扫”免费获取 方案+报价!
如有侵权请通知删除,谢谢!
本文转自;三年全职 Rust 游戏开发,真要放弃 Rust 吗?-腾讯云开发者社区-腾讯云 (tencent.com)