Bevy:Rust构建的数据驱动游戏引擎和APP框架

2020-08-12 06:17:19

Bevy是一个令人耳目一新的简单数据驱动游戏引擎和应用程序框架,由Rust构建。它永远是免费和开源的!

Bevy有许多我认为与其他引擎不同的功能:

高效编译时间:使用";FAST编译";配置,预计更改可在约0.8-3.0秒内编译。

精灵:将单个图像渲染为精灵,从精灵工作表渲染,并动态生成新的精灵工作表。

场景:将ECS World保存为人类可读的场景文件,并将场景文件加载到ECS World。

多个渲染后端:Vulkan、DirectX 12和Metal(由于wgpu,更多渲染后端即将推出)。

尽管如此,Bevy仍处于非常早期的阶段。我认为它正处于原型阶段:功能缺失,API将改变,文档稀少。我还不推荐在严肃的项目中使用Bevy,除非你愿意处理差距和不稳定问题。

希望在这一点上,你要么(1)对Bevy感兴趣,要么(2)不再阅读了。如果你想现在就开始,贝弗书是最好的入门之处。您也可以继续阅读,以了解Bevy的当前状态以及我们想要将其带到哪里。

给读者的快速提示:在本文中,您将找到格式如下的文本:Texture。

此格式表示文本是链接到API文档的铁锈类型。我鼓励你点击任何你感兴趣的东西!

首先,让我们看看Bevy App到底是什么样子。最简单的App如下所示:

就是这样!这个App没有引入任何功能,实际上什么也不做。运行该程序将立即终止。我们可以通过这样做让它变得更有趣:

Add_default_plugins()添加了您可能希望从游戏引擎获得的所有功能:2D/3D渲染器、资源加载、UI系统、窗口、输入等。

Fn main(){App::Build()。Add_plugin(CorePlugin::Default())。Add_plugin(InputPlugin::Default())。Add_plugin(WindowPlugin::Default())。Add_plugin(RenderPlugin::Default())。Add_plugin(UiPlugin::default())/*此处有更多插件...。为简明起见而略去*/。Run();}。

当然,您也可以创建自己的插件。事实上,所有引擎和游戏逻辑都是使用插件构建的。希望现在您能理解我们所说的模块化是什么意思:您可以根据项目的独特需求自由添加/删除插件。然而,为了简单起见,我预计大多数人会坚持使用add_default_plugins(),至少一开始是这样。

所有Bevy引擎和游戏逻辑都构建在自定义实体组件系统(简称ECS)之上。实体组件系统(Entity Component Systems)是一种涉及将数据拆分成组件的软件范例。实体是分配给组件组的唯一ID。例如,一个实体可能具有Position和Velocity组件,而另一个实体可能具有Position和UI组件。系统是在一组特定组件类型上运行的逻辑。您可能有一个在具有位置和速度组件的所有实体上运行的移动系统。

ECS模式通过强制您将应用程序数据和逻辑分解为其核心组件来鼓励干净、解耦的设计。

与其他需要复杂生命周期、特征、构建器模式或宏的Rust ECS实现不同,bevy ECS对所有这些概念都使用普通的Rust数据类型:

关于ECS范式的介绍已经很多了,所以我将把ECS的最新进展留给读者作为练习,直接跳到是什么让Bevy的ECS如此特别的问题上进行介绍:。

我要在这里提出一个狂野的(且无法证实的)主张:Bevy ECS是现有的最符合人体工程学的ECS:

使用bevy::prelude::*;struct Velocity(F32);struct Position(F32);//此系统使用Position和Velocity Components FN设置生成实体(mut命令:命令){命令。Screen((Position(0.0),Velocity(1.0),))。Screen((Position(1.0),Velocity(2.0),));}//此系统在每个实体上运行,具有位置和Velocity组件Fn移动(Position:mut<;position>;,ocity:&;Velocity){位置。0+=速度。0;}//应用入口点。希望您能从上面的示例中认出它!Fn main(){App::Build()。ADD_STARTUP_SYSTEM(安装。System())。ADD_SYSTEM(移动。System())。Run();}。

这是一个完整的自给自足的bevy应用程序,具有自动并行系统调度和全局更改检测功能。在我看来,你不会找到任何更清晰或符合人体工程学的ECS。构建游戏(和引擎)涉及编写大量系统,因此我投入了大量资金使ECS代码易于编写和阅读。

ECS范式如此受欢迎的原因之一是,它有可能让游戏逻辑变得超级快,主要是因为这两个原因:

迭代速度:组件被紧密地打包在一起,以针对缓存位置进行优化,这使得遍历它们的速度非常快

Bevy ECS尽其所能地做好了这两件事。根据流行的ECS_BENCH基准测试,BEVY ECS是速度最快的Rust ECS,遥遥领先:

注意,ECS_BENCH是一个单线程基准测试,所以它没有说明这些框架的多线程功能。和往常一样,请注意ECS_BENCH是一个微型基准测试,它不能说明复杂游戏的性能。ECS性能空间有很大的细微差别,上面的每个ECS实现在不同的工作负载下都会有不同的性能。

如果有人想要仔细检查我的方法,我已经在这里推送了我的ECS_BENCH版本。在一段合理的时间内,如果有人报告问题或我的结果(平均而言)不可重现,我会在这里发布更新。

现在你可能在想";[email protected],所以bevy ECS有很好的性能和人体工程学,但这肯定意味着你必须在功能上做出妥协!";

//对于每个系统";在包含给定组件的每个实体上运行一次fn系统(位置:mut<;position>;,速度:&;Velocity){//做点什么}。

//此查询系统与上面的系统相同,但允许您控制迭代fn系统(mut query:query<;(&;position,&;mut Velocity)>;){for(position,mut ocity)in&;query。ITER(){//做点什么}}。

//添加的<;T>;查询仅在已添加给定组件的FN system(mut query:query<;Added<;Position>;>;){表示&;mut查询中的职位时运行。Iter(){//做某事}}//突变的<;T>;查询仅在给定组件发生突变的情况下运行FN系统(mut query:query<;muted<;position>;){表示&;mut查询中的位置。Iter(){//Do Something}}//Change<;T>;查询仅在给定组件已添加或突变的FN系统(mut query:query<;Changed<;Position>;>;){表示&;mut查询中的位置时运行。Iter(){//Do Something}}//query.removed<;T>;()将迭代删除组件T所在的每个实体。对于查询中的实体,此更新FN系统(mut query:query<;&;position>;){。已删除::<;Velocity>;(){//做点什么}}。

FN SYSTEM(mut wall_query:query<;&;wall>;,mut player_query:query<;&;player>;){适用于&;mut player_query中的播放器。Iter(){for wall in&;mut wall_query。ITER(){如果玩家。Collides_with(Wall){println!(";ouch";);}

对于&;mut Entity_Query中的实体,FN SYSTEM(MUT ENTITY_QUERY:Query<;Entity>;,MUT Player_Query:Query<;&;Player>;){。Iter(){如果让Some(Player)=Player_Query。Get::<;player>;(实体){//当前实体有播放器组件}。

//res和ResMut访问全局资源FN系统(时间:RES<;Time>;,Score:ResMut<;Score>;){//做某事}//您可以在任何系统类型的FN系统中使用资源(Time:Res<;Time>;,MUT Query:Query<;&;Position>;){//做某事}FN System(Time:RES<;Time>;,&;Position。

//每个系统的本地<;T>;资源是唯一的。同一系统的两个实例将各自拥有自己的资源。本地资源会自动初始化为其默认值。FN系统(STATE:LOCAL<;State>;,&;Position){//做点什么}。

//仅在具有或不具有给定组件fn系统的实体上运行(mut查询:查询<;没有<;父级,&;职位>;>;){表示&;mut查询中的职位。ITER(){//做点什么}}。

//必须在具有World和Resources FN系统独占访问权限的主线程上运行的系统(world:&;mut World,resources:&;mut Resources){//执行一些操作}。

//调度器提供阶段作为按顺序fn main(){App::Build()//将系统添加到默认阶段:";update";运行系统集的一种方式。ADD_SYSTEM(移动。System())//在";更新";之后创建一个新阶段。Add_Stage_After(";UPDATE";,";DO_Things";)。Add_System_to_Stage(";Do_Things";,某事。系统())}。

//使用命令将World和Resource更改排队,这些更改将在当前阶段FN系统结束时应用(mut命令:命令){命令。Spawn((Position(0.0),Velocity(1.0));}//命令也可以与其他类型的FN系统一起使用(mut命令:命令,time:res<;time>;,mut query:query<;&;position>;){//做点什么}。

能够像系统一样直接使用Rust函数可能感觉像是魔术,但我保证不是!您可能已经注意到,我们在应用程序中注册系统时会这样做:

System()调用获取SOME_SYSTEM函数指针并将其转换为Box<;dyn系统>;。这是可行的,因为我们为匹配特定函数签名集的所有函数实现了IntoQuerySystem特征。

Bevy ECS实际上使用的是极简主义HECS ECS的严重分叉版本。HECS是一种高效的单线程原型ECS。它提供核心World、原型和内部查询数据结构。Bevy ECS在顶部添加了以下内容:

功能系统:HECS实际上根本没有系统的概念。您只需直接在World上运行查询即可。Bevy增加了使用普通Rust函数定义可移植、可调度系统的能力。

资源:HECS没有唯一/全局数据的概念。在构建游戏时,通常需要这样做。Bevy添加了资源集合和资源查询。

并行调度器:HECS是单线程的,但它的设计允许在顶部构建并行调度器。Bevy ECS添加了一个自定义的依赖项感知调度器,该调度器构建在上面提到的函数系统之上。

优化:HECS已经相当快了,但是通过修改它的一些内部数据访问模式,我们能够显著提高性能。这使它从足够快的&34;变成了最快的";(参见上面的基准来比较bevy ECS和vanilla HECS)。

查询包装器:ECS导出的查询实际上是HECS查询的包装器。它在多线程上下文中提供对World的安全、作用域访问,并改进迭代的人体工程学。

变更检测:自动(高效)跟踪组件添加/删除/更新操作,并在查询界面中公开它们。

稳定的实体ID:几乎每个ECS(包括HEC)都使用不稳定的实体ID,不能用于序列化(场景/保存文件)或联网。在bevy ECS中,实体ID是全局唯一且稳定的。您可以在任何上下文中使用它们!

在不久的将来,我将提交一期关于HECS GIT回购的问题,以便上游他们希望从Bevy ECS进行的任何改变。我有一种感觉,他们不会想要像函数系统和并行调度这样的高级东西,但我想我们会拭目以待!

Bevy有一个基于flex box模型的自定义但熟悉的UI系统。嗯..。半定制的,稍后会详细介绍。一开始,我曾深思熟虑地考虑使用Rust生态系统中众多优秀的预制UI解决方案之一。但是,这些框架中的每一个都感觉在某种程度上与BEVY核心的数据驱动的ECS方法是分开的。如果我们采用像Druid这样的框架,它在设计上是同类中最顶尖的,然后把它硬塞到bevy数据/事件模型中,这将损害Druid的设计,而Bvy+Druid最终会比仅仅使用Druid作为一个独立的框架更缺乏说服力。

我决定,只有完全接受贝维的做事方式,才能希望把一些引人入胜的东西带到谈判桌上来。

Bevy UI直接使用位于Bevy核心的现有ECS、层次、变换、事件、资源和场景系统。因此,bevy UI自动获得UI场景文件热重新加载、异步纹理加载和更改检测等功能。共享架构意味着对这些系统的任何改进都可以直接提供给bevy UI。我还不相信这种方法会产生最好的UI框架,但我相信它会在Bevy App的上下文中产生最好的UI框架。

我们仍处于实验阶段,我预计有些事情会改变,但我们到目前为止发现的模式是非常有希望的。还请记住,目前编写漂亮UI的最佳方式是使用代码,但我们正在为场景设计一种新的文件格式,它应该会使声明性的、基于文件的UI组合比现在好得多。

在BEVY中,UI元素只是一个带有节点组件的ECS实体。节点是具有宽度和高度的矩形,并使用Bevy中其他地方使用的相同变换组件进行定位。Style组件用于确定如何呈现节点、调整节点大小和定位节点。

添加新节点(包含所有必需组件)的最简单方法如下所示:

NodeComponents是一个";组件包,Bevy使用它来简化各种";类型";实体的生成。

在布局方面,Bevy使用了一个非常棒的100%Rust Flexbox实现,称为Stretch。STRETCH提供了根据FlexBox等级库在二维空间中定位矩形的算法。Bevy公开了上面提到的样式组件内部的FLEX属性,并使用拉伸输出的位置和大小呈现矩形。Bevy使用自己的z分层算法将元素堆叠在一起,但基本上与HTML/CSS使用的算法相同。

命令。生成(节点组件{Style:Style{Size:Size::New(val::px(100.0),val::px(100.0)),..。Default::Default()},Material:Material。Add(Color::RGB(0.08,0.08,1.0)。变成()),..。Default::Default()})。Spawn(NodeComponents{style:style{size:size::new(val::Percent(40.0),val::Percent(40.0)),..。Default::Default()},Material:Material。Add(Color::RGB(1.0,0.08,0.08)。变成()),..。Default::default()});

命令。Spawn(NodeComponents{style:style{size:size::new(val::Percent(40.0),val::Percent(40.0),position_type:PositionType::Absolute,Position:RECT{top:val::px(10.0),right:val::px(10.0),..。Default::Default()},..。Default::Default()},Material:Material。Add(Color::RGB(0.08,0.08,1.0)。变成()),..。Default::default()});

就像任何其他实体一样,节点可以有子节点。子对象相对于其父对象进行定位和缩放。默认情况下,孩子将始终出现在父母面前。

命令。Spawn(NodeComponents{style:style{size:size::new(val::Percent(60.0),val::Percent(60.0),position_type:PositionType::Absolute,Position:RECT{top:val::px(10.0),right:val::px(10.0),..。Default::Default()},..。Default::Default()},Material:Material。Add(Color::RGB(0.08,0.08,1.0)。变成()),..。Default::Default()})。WITH_CHILD(|PARENT|{PARENT。Spawn(NodeComponents{style:style{size:size::new(val::px(50.0),val::px(50.0)),..。Default::Default()},Material:Material。Add(Color::RGB(0.5,0.5,1.0)。变成()),..。Default::default()});});

我不会在这里介绍FlexBox是如何工作的,但是您可以使用在Web上下文中使用的所有相同属性。下面是一个示例,说明如何使两个节点在其父节点内垂直和水平居中:

命令。派生(节点组件{Style:Style{Size:Size::New(val::Percent(100.0),val::Percent(100.0)),JUSTUSTY_CONTENT:JustifyContent::Center,Align_Items:AlignItems::Center,..。Default::Default()},Material:Material。Add(Color::RGB(0.04,0.04,0.04)。变成()),..。Default::Default()})。WITH_CHILD(|PARENT|{PARENT。Spawn(NodeComponents{style:style{size:size::new(val::px(80.0),val::px(80.0)),..。Default::Default()},Material:Material。Add(Color::RGB(0.08,0.08,1.0)。变成()),..。Default::Default()})。Spawn(NodeComponents{style:style{size:size::new(val::px(80.0),val::px(80.0)),..。Default::Default()},Material:Material。Add(Color::RGB(1.0,0.08,0.08)。变成()),..。Default::default()});});

节点还可以具有文本和图像组件,这会影响节点的推断大小。

命令。从bevy UI!";派生(TextComponents{text:text{value:";Hello from bevy UI!";。To_string(),font:ASSET_SERVER。加载(";FiraSans-Bold.ttf";)。UnWrap(),样式:TextStyle{font_size:25.0,color:color::White,},},..。Default::Default()})。生成(图像组件{STYLE:STYLE{SIZE:SIZE::NEW(val::px(200.0),val::Auto),Position_TYPE:PositionType::Absolute,Position:Rect{TOP:val::px(10.0),Right:val::px(10.0),..。Default::Default()},..。Default::Default()},Material:Material。添加(ASSET_SERVER。加载(";bevy_logo.png";)。展开()。变成()),..。Default::default()});

具有交互组件的节点将跟踪交互状态。您可以通过以下方式轻松构建按钮之类的小部件:

例如,下面的系统仅在交互状态已更改的按钮上运行:

FN系统(_Button:&;Button,交互:突变<;交互>;){匹配*交互{交互::单击=>;println!(";单击";),交互::hovered=>;println!(";hovered";),交互::无=>;{},}}。

精灵工作表(也称为纹理地图集)可用于动画、平铺集或仅用于优化的精灵渲染。

假设Texture_atlas=TextureAtlas::from_grid(Texture_Handle,Texture。Size,7,1);设Texture_atlas_Handle=Texture_atlases。Add(Texture_Atlas);命令。Spawn(SpriteSheetComponents{Texture_atlas:Texture_atlas_Handle,Sprite:TextureAtlasSprite::New(0),..。Default::default()});

精灵通常作为单独的文件生成。Bevy可以动态地将它们组合成单一的精灵工作表!

表示精灵句柄中的精灵句柄。Iter(){设Texture=纹理。获取(&;句柄)。UNWRAP();Texture_atlas_builder。Add_Texture(Handle,&;Texture);}让Texture_atlas=Texture_atlas_builder。抛光(&A)MUT纹理。

.