与其它大部分游戏引擎不同,Bevy 采用了一种名为ECS 的编程范式,ECS 将数据和行为分离。Bevy将存储我们的所有数据,并为我们管理所有独立的功能单元。代码会在适当的时候运行。
那么,ECS代表什么意思?
缩写 | 全称 |
---|---|
E | Entity,实体 |
C | Component,组件 |
S | System,系统 |
如果我们有面向对象语言的基础,那么可以这么理解ECS:对象实例,类,函数。
好的,我们从两个视角(面向对象,面向ECS)来看看游戏的运行逻辑。
面向对象视角
如果我们从面向对象视角看游戏的运行逻辑,我们可能会这样实现。
定义对象:
rust
struct Sprite {
texture: String,
position: Vec2,
}
定义对象的行为:
rust
impl Sprite {
fn new(texture: String, position: Vec2) -> Self {
Self { texture, position }
}
fn set_position(&mut self, position: Vec2) {
self.position = position;
}
}
运行对象相关的逻辑(创建示例,设置行为):
rust
fn main() {
let mut sprite = Sprite::new("boy".to_string(), Vec2::ZERO);
sprite.set_position(Vec2::new(400.0, 200.0));
}
这完全是面向对象的做法,首先定义对象的字段,然后定义对象支持的行为,然后在合适的时机运行对象的逻辑。
但Bevy ,用了完全不一样的做法,即ECS。
ECS视角
定义对象:
rust
pub struct Sprite {
pub image: Handle<Image>,
//other code
}
rust
pub struct Transform {
pub translation: Vec3,
// other code
}
创建对象:
rust
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
Sprite::from_image(asset_server.load("bevy_bird_dark.png")),
Transform::from_xyz(200.0, 200.0, 0.0),
));
}
定义对象行为:
rust
fn moving_sprite(mut sprite_query: Option<Single<(&mut Transform)>>) {
if let Some(mut sprite) = sprite_query {
sprite.0.translation.x = 0.0;
sprite.0.translation.y = 0.0;
}
}
嗯~~~,这只是代码的一部分,从目前的代码看,和面向对象的视角有很多不一样。现在,让我们深入一下,看看Bevy如何运行一个简单的游戏逻辑。
ECS的游戏逻辑
从简单开始
rust
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.run();
}
在上一篇文章中,我们已经介绍了这段代码的功能,那就是只显示一个窗口。
创建精灵
rust
#[derive(Component)]
struct SpriteMark;
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
commands.spawn((
Sprite::from_image(asset_server.load("bevy_bird_dark.png")),
Transform::from_xyz(200.0, 200.0, 0.0),
SpriteMark,
));
}
#[derive(Component)]
是一个声明,这个声明表示我们创建的struct
是一个Component ,例如上面的代码,我们创建了一个SpriteMark
的Component ,这个Component 有什么用?大家看代码实现会发现,这个SpriteMark
没有任何数据。一般情况下,这种Component 我们称为标记组件,标记组件的唯一作用,就是方便System 去查找我们的Entity,这个以后再讲。
fn setup(mut commands: Commands, asset_server: Res<AssetServer>)
该函数就一个作用,创建Entity ,我们看下实际的创建逻辑。commands.spawn(Camera2d);
生成一个2D相机,游戏世界中,唯一不可缺少的组件就是相机,因为有了相机,我们才能看见游戏世界。
rust
commands.spawn((
Sprite::from_image(asset_server.load("bevy_bird_dark.png")),
Transform::from_xyz(200.0, 200.0, 0.0),
SpriteMark,
));
上述代码,便是生成我们的精灵实体。第二行代码生成的是精灵纹理,可以看到加载了一个文件作为精灵的纹理,第三行代码定义了精灵的位置,第四行,创建了我们精灵的标记。
添加到系统
rust
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
之前我们定义的setup
函数,需要添加到Bevy 的运行逻辑中。此处,我们使用了add_systems(Startup, setup)
,把setup
添加到了游戏的运行逻辑中。Starup
会在游戏启动的时候运行一次,也就是说,setup
函数,会在我们游戏运行的时候,初始化阶段,执行一次。
Startup
,在Bevy 中,叫做调度。而setup
函数,当我们使用add_systems
时候,它就变成了ECS 中的System,也就是系统。
Bevy 中最常见的一个错误,就是定义了一个函数或者运行逻辑,而没有添加到System中。
此时,我们的代码已经可以运行了,我们看看效果。
哦,对了,忘记告诉各位要在项目的某个目录下,放入你喜欢的资源。
再复杂一点
如果仅仅在初始位置放置一个精灵,这并不吸引人。那么我们稍微添加一个逻辑,当点击键盘M键的时候,移动一下精灵。
rust
fn moving_sprite(mut sprite_query: Option<Single<(&mut Transform, &SpriteMark)>>) {
if let Some(mut sprite) = sprite_query {
sprite.0.translation.x = 0.0;
sprite.0.translation.y = 0.0;
}
}
上述代码唯一值得研究的地方就是函数参数, mut sprite_query: Option<Single<(&mut Transform, &SpriteMark)>>
,我们定义了一个单项查询Option<Single<>>
,在Bevy 中,获取Entity 的方式就是通过查询,你可以理解为查找满足某个条件的Entity 。这个时候,我们终于看到了SpriteMark
作用,如果我们在编写查询条件的时候,有点点困难,那么我们就可以定义个Component ,专门作为Entity 的标记。这个参数翻译过来就是:帮我查询一个,既带有Transform
也带有SpriteMark
的Entity 。因为SpriteMark
的作用,所以我们查询到的Entity
,就是我们之前的创建的精灵。
好的,接下来函数实现,就是移动精灵的位置。
最后,别忘了,添加到Bevy的系统:
rust
App::new()
//other code
.add_systems(Update, moving_sprite.run_if(input_pressed(KeyCode::KeyM)))
//other code
.add_systems(Update, moving_sprite.run_if(input_pressed(KeyCode::KeyM)))
,这次,我们换了一个不一样的调度------Update
,该调度和游戏的帧率保持一致,也就是在游戏运行期间,会一直运行,游戏如果是一秒钟60帧,那么该调度就调度System
一秒钟60次。moving_sprite.run_if(input_pressed(KeyCode::KeyM))
,moving_sprite
的实际执行,会在按下键盘M键的时候执行。
OK,现在,我们的游戏没有那么无聊了,在游戏运行之后,点击M键,会移动一下了。
总结
好的,我们已经了解基本的Bevy 运行方式,即ECS 。可能目前还不是那么深入,不过没关系,后续随着文章的更新,对ECS会越来越深入的。