23 个版本
0.2.22 | 2024年5月13日 |
---|---|
0.2.21 | 2024年4月14日 |
0.2.18 | 2024年3月12日 |
0.1.3-beta.2 | 2024年3月5日 |
#606 在 游戏开发
231 每月下载量
63KB
1.5K SLoC
Zero ECS
Zero ECS 是一个实体组件系统,编写时有 4 个目标
- 仅使用零成本抽象 - 不使用 dyn 和 Box 等东西 零成本抽象。
- 不使用 unsafe rust 代码。
- 非常用户友好。用户应该尽可能少地编写样板代码。
- 非常快
它通过在编译时生成所有代码,结合宏和构建脚本来实现这一点。
说明
创建新项目
cargo new zero_ecs_example
cd zero_ecs_example
添加依赖
cargo add zero_ecs
cargo add zero_ecs_build --build
您的 Cargo.toml 应该看起来像这样
[dependencies]
zero_ecs = "*"
[build-dependencies]
zero_ecs_build = "*"
创建 build.rs
touch build.rs
编辑 build.rs
以调用 zero_ecs 的构建生成代码。
use zero_ecs_build::*;
fn main() {
generate_ecs("src/main.rs"); // look for components, entities and systems in main.rs
}
这将根据 main.rs 中的组件、实体和系统生成实体组件系统。它接受通配符,因此您可以使用通配符。
use zero_ecs_build::*;
fn main() {
generate_ecs("src/**/*.rs"); // look in all *.rs files in src.
}
使用 ECS
包含 ECS
在 main.rs
中按如下方式包含 ECS
include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs"));
组件
定义一些组件
位置和速度有 x 和 y
#[component]
struct Position(f32, f32);
#[component]
struct Velocity(f32, f32);
在 ECS 中,通常使用组件来“标记”实体,以便在系统中单独识别这些实体。
#[component]
struct EnemyComponent;
#[component]
struct PlayerComponent;
实体
实体是一组组件的集合,它们也可能被称为原型、捆绑或游戏对象。注意,一旦“在” ECS 中。实体只是一个可以复制的 ID。
在我们的示例中,我们定义了一个敌人和一个玩家,他们都有位置和速度,但可以通过他们的“标记”组件来区分。
#[entity]
struct Enemy {
position: Position,
velocity: Velocity,
enemy_component: EnemyComponent,
}
#[entity]
struct Player {
position: Position,
velocity: Velocity,
player_component: PlayerComponent,
}
系统
系统运行应用程序的逻辑。它们可以接受引用、可变引用和查询。
在我们的示例中,我们可以创建一个系统,它简单地打印所有实体的位置
#[system]
fn print_positions(world: &World, query: Query<&Position>) {
world.with_query(query).iter().for_each(|position| {
println!("Position: {:?}", position);
});
}
解释
- world: &World - 由于系统不修改任何内容,它可以是不可变引用
- query: Query<&Position> - 我们想查询世界中的所有位置
- world.with_query(query).iter() - 创建所有位置组件的迭代器
创建实体和调用系统
在我们的 fn main
中,将其改为创建 10 个敌人和 10 个玩家,并添加 systems_main(&world);
来调用所有系统。
fn main() {
let mut world = World::default();
for i in 0..10 {
world.create(Enemy {
position: Position(i as f32, 5.0),
velocity: Velocity(0.0, 1.0),
..Default::default()
});
world.create(Player {
position: Position(5.0, i as f32),
velocity: Velocity(1.0, 0.0),
..Default::default()
});
}
systems_main(&world);
}
现在运行程序,将打印实体的位置。
更高级
继续我们的示例
修改系统
大多数系统会修改世界状态,并需要额外的资源,如纹理管理器、时间管理器、输入管理器等。一种良好的做法是将它们组合在一个 Resources 结构体中。(但不是必需的)
struct Resources {
delta_time: f32,
}
#[system]
fn apply_velocity(
world: &mut World, // world mut be mutable
resources: &Resources, // we need the delta time
query: Query<(&mut Position, &Velocity)>, // position should be mutable, velocity not.
) {
world
.with_query_mut(query) // we call with_query_mut because it's a mutable query
.iter_mut() // iterating mutable
.for_each(|(position, velocity)| {
position.0 += velocity.0 * resources.delta_time;
position.1 += velocity.1 * resources.delta_time;
});
}
我们还必须更改主函数以包含资源调用。
let resources = Resources { delta_time: 0.1 };
systems_main(&resources, &mut world);
销毁实体
假设我们想创建一条规则,如果玩家和敌人之间的距离小于 3 个单位,他们都应该被销毁。这是我们的实现方式
#[system]
fn collide_enemy_and_players(
world: &mut World, // we are destorying entities so it needs to be mutable
players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities
enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies
) {
let mut entities_to_destroy: Vec<Entity> = vec![]; // we can't (for obvious reasons) destroy entities from within an iteration.
world
.with_query(players)
.iter()
.for_each(|(player_entity, player_position, _)| {
world
.with_query(enemies)
.iter()
.for_each(|(enemy_entity, enemy_position, _)| {
if (player_position.0 - enemy_position.0).abs() < 3.0
&& (player_position.1 - enemy_position.1).abs() < 3.0
{
entities_to_destroy.push(*player_entity);
entities_to_destroy.push(*enemy_entity);
}
});
});
for entity in entities_to_destroy {
world.destroy(entity);
}
}
获取 & At 实体
Get 与 query 相同,但需要一个实体。At 与 query 相同,但需要一个索引。
假设你想要一个跟随玩家的实体。这是你如何实现它的
定义一个伴随组件
#[component]
struct CompanionComponent {
target_entity: Option<Entity>,
}
定义伴随实体。它有一个位置和一个伴随组件
#[entity]
struct Companion {
position: Position,
companion_component: CompanionComponent,
}
现在我们需要编写伴随系统。对于每个伴随,我们需要检查它是否有目标。如果有目标,我们需要检查目标是否存在(它可能已被删除)。如果目标存在,我们获取目标的 位置值 并使用该值设置伴随的位置。
我们需要查询伴随及其位置,作为可变查询。我们还需要查询具有位置的每个实体。这意味着伴随理论上可以跟随它自己。
#[system]
fn companion_follow(
world: &mut World,
companions: Query<(&mut Position, &CompanionComponent)>,
positions: Query<&Position>,
) {
实现:我们不能简单地遍历伴随,获取目标位置并更新位置,因为我们只能有一个可变借用(除非我们使用 unsafe 代码)。
我们可以像销毁实体那样做,但这会很慢。
解决方案是使用索引迭代,只短暂借用所需的内容
#[system]
fn companion_follow(
world: &mut World,
companions: Query<(&mut Position, &CompanionComponent)>,
positions: Query<&Position>,
) {
for companion_idx in 0..world.with_query_mut(companions).len() {
// iterate the count of companions
if let Some(target_position) = world
.with_query_mut(companions)
.at_mut(companion_idx) // get the companion at index companion_idx
.and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none
.and_then(|companion_target_entity| {
// then get the VALUE of target position (meaning we don't use a reference to the position)
world
.with_query(positions)
.get(companion_target_entity) // get the position for the companion_target_entity
.map(|p| (p.0, p.1)) // map to get the VALUE
})
{
if let Some((companion_position, _)) =
world.with_query_mut(companions).at_mut(companion_idx)
// Then simply get the companion position
{
// and update it to the target's position
companion_position.0 = target_position.0;
companion_position.1 = target_position.1;
}
}
}
}
待办事项
- 重新使用已删除实体的 ID
依赖关系
~0.7–1.2MB
~27K SLoC