用白头蛇克隆蛇

2020-11-06 07:10:03

Bevy最近被迅速采用,但学习资源仍然相当稀缺。这是一种尝试,试图提供继Bevy Book之后的NextStep。最终产品如下所示:

它大约有300行锈迹,所以系好安全带;这是一段很长的路程。如果你想快进到完成的代码,代码就在这里。每个部分的顶部都有不同之处,这应该可以更容易地确定在不清楚的情况下将代码片段放在哪里。

我们将从一款什么都不做的应用程序开始,从Bevy Book开始。运行Cargo New Bevy-Snake,然后将以下内容放入您的主机中。rs:

我们需要将bevy作为依赖项添加到Cargo.toml中,而且因为我能够预测本教程的未来,所以让我们继续在其中添加rand,这样当时间到来时我们就可以使用它了。

我们将创建一个2D游戏,它需要很多不同的系统;一个用于创建窗口,一个用于执行渲染循环,一个用于处理输入,一个用于处理精灵等等。幸运的是,Bevy的默认插件为我们提供了所有这些功能:

不过,Bevy的默认插件不包括摄像头,所以让我们插入一个2d摄像头,我们将通过创建第一个系统来设置它:

命令用于排队命令,以改变世界和资源。在这种情况下,我们正在生成一个新的实体,包含2D摄像机组件。准备好迎接一些迷人的魔力吧:

我们所要做的就是在我们的函数上调用.system(),而bevy将在启动时使用命令param自动神奇地调用它。再次运行该应用程序,您应该会看到一个空窗口,如下所示:

让我们试着把蛇头放进去。我们将定义两个结构:

蛇头只是一个空的结构,我们将把它用作一个组件,它有点像一个标签,我们将放在一个实体上,然后我们可以在稍后通过查询带有蛇头组件的实体来找到那个实体。像这样的空结构是bevy中的常见模式,组件通常不需要它们的任何运行状态。材料将成为一种资源,储存我们现在将用来制作蛇头的材料,并最终用于蛇段和食物。

Head_Material句柄应该在游戏设置时创建,所以接下来让我们通过修改设置函数来执行此操作:

FN Setup(MUT Commands:Commands,MUT Material:ResMut<;Assets<;ColorMaterial>;>;){commands.spawn(Camera2dComponents::default());命令.INSERT_RESOURCE(材质{Head_Material:Material s.add(颜色::rgb(0.70.7,0.7).into()),});}。

BEVY在注册系统时需要对参数进行特定排序。命令→资源→组件/查询。如果在扰乱系统后出现神秘的编译时错误,请检查您的订单。

Materials.add将返回一个句柄<;ColorMaterial>;。我们使用这个新句柄创建一个Materials结构。稍后,当我们尝试访问具有材料类型的资源时,bevy会发现我们创建的这个结构。现在,我们将在一个新系统中创建蛇头实体,您可以看到使用该资源是什么样子:

FN GAME_SETUP(mut Commands:Commands,Material:res<;Materials>;){Commands.spwn(SpriteComponents{Material:Materals.head_Material al.clone(),Sprite:Sprite::New(vec2::New(10.0,10.0)),.Default::Default()}).with(Snakehead);}。

这里我们有一个新系统,它将查找材料类型的资源。然后,它将生成一个新实体,将SpriteComponents和Snakehead作为组件。为了创建SpriteComponents,我们将句柄传递给前面创建的颜色材质,并为Sprite赋予大小(10,10)。让我们将该系统添加到我们的应用程序构建器中:

我们需要一个新的阶段,而不是再次调用add_startup_system,原因是我们需要使用在setupfunction中插入的材料。运行此命令后,您应该会在屏幕中央看到一个蛇头:

好吧,也许叫它蛇头有点夸张,你会看到一个10x10的白色精灵。

蛇并不是一个没有运动的游戏,所以让我们移动一下头部。我们稍后会担心输入的问题,因为现在我们的目标只是让头部移动。因此,让我们创建一个系统,让所有的蛇头都向上移动:

Fn Snake_Move(mut head_position:查询<;(&;蛇头,&;mut转换)>;){for(_head,mut转换)in head_postions.iter_mut(){*Transform.Translation.y_mut()+=2。;}}。

这里的主要新概念是查询类型。我们可以使用它来遍历同时具有蛇头组件和Transform组件的所有实体。我们不必担心实际创建该查询,bevy将负责创建它并使用它调用我们的函数,这是ECSMagic的一部分。因此,让我们将该系统添加进来,看看会发生什么:

您可能会对该转换组件感到疑惑。当我们产生蛇头的时候,我们没有给它一个变换,那么为什么我们能够找到一个有蛇头和变换组件的实体呢?实际上,SpriteComponents是一个组件捆绑包。对于SpriteComponents,这意味着我们在一系列其他组件(Sprite、网格、绘制、旋转、缩放等)中获得了一个变换组件。

让我们修改蛇的移动系统,以实际允许我们引导蛇:

Fn Snake_Move(keyboard_input:res<;input;keycode>;,mut head_Positions:Query<;(&;Snakehead,&;mut Transform)>;,){for(_head,mut Transform)in head_postions.iter_mut(){if keyboard_input.press(keycode:Left){*Transform.Translation.x_mut()-=2;}if keyboard_input.press(keycode::right){*Transform.Translation.x_mut()+=2.;}if keyboard_input.press(keycode::down){*Transform.Translation.y_mut()-=2.;}if keyboard_input.press(keycode::up){*Transform.Translation.y_mut()+=2.;}

到目前为止,我们一直使用窗口坐标,其工作方式是(0,0)是中间,单位是像素。蛇游戏一般使用网格,所以如果我们的蛇游戏是10x10,我们的窗口将会非常小。让我们通过使用自己的定位和大小来让我们的生活更轻松一些。然后我们就可以使用处理这些数据到窗口坐标转换的系统了。

我们会把我们的网格设为10x10。我们将这些定义为文件顶部的常量:

#[Derive(Default,Copy,Clone,Eq,PartialEq,Hash)]结构位置{x:i32,y:i32,}结构大小{宽度:f32,高度:f32,}实施大小{pub fn square(x:f32)->;self{self{width:x,Height:x,}。

非常简单,使用辅助方法可以获得等宽等高的尺寸。位置衍生出了一些以后会有用的特征,所以我们不必一直回到位置上来。大小可能只有一个浮点,因为所有物体最终都会有相同的宽度和高度,但感觉不对,所以我给它一个宽度和一个高度。让我们将这些组件添加到蛇头木卒中:

命令.spwn(SpriteComponents{Material:Material:Materials.head_Material al.clone(),Sprite:Sprite::New(vec2::New(10.0,10.0)),.Default::Default()}).with(蛇头).with(Position{x:3,y:3})//<;--.with(Size:Square(0.8));//<;--。

这些组件目前没有任何作用,让我们从将我们的大小转换为精灵大小开始:

Fn Size_Scaling(windows:res<;Windows>;,mut q:Query<;(&;Size,&;mut Sprite)>;){for(size,mut Sprite)in q.iter_mut(){let Window=windows.get_Primary().unwire();Sprite.size=ve2::new(size.width as F32/Arena_width as F32*window.width()as F32,size.high as F32*window.high()as F32,);}}。

调整大小的逻辑是这样的:如果某个对象在40个网格中的宽度为1,而窗口的宽度为400px,则它的宽度应该为10。接下来,我们可以执行定位系统:

Fn Position_Translate(Windows:res<;Windows>;,mut q:Query<;(&;position,&;mut Transform)>;){fn Convert(p:F32,Bound_Window:F32,Bound_Game:f32)->;F32{p/Bound_Game*Bound_Window-(Bound_Window/2.)+(Bound_Window/Bound_Game/2.)}let Window=windows.get_Primary().unwire();对于q.iter_mut()中的(pos,mut变换){转换=ve3::new(Convert(pos.x as F32,window.width()as F32,Arena_width as F32),Convert(pos.y as F32,window.high()as F32,Arena_Height as F32),0.0,);}。

位置平移:如果一个项目的x坐标在我们的系统中是5,我们系统中的宽度是10,窗口宽度是200,那么坐标应该是5/10*200-200/2。我们减去窗口宽度的一半,因为我们的坐标系是从左下角开始的,而平移是从中心开始的。然后我们添加一个瓷砖大小的一半,因为我们希望精灵的左下角位于瓷砖的左下角,而不是中心。

现在当你运行它的时候,你应该在左下角看到一条被压扁的小蛇:

这里最明显的问题是蛇被压扁了。另一个问题是我们破坏了输入处理。我们将首先修复输入处理,但请放心,我们会回到我们被压扁的小蛇身上,让它恢复到正确的比例。

现在我们已经设置了网格,我们需要更新Snake_movementsystem。我们以前使用的是变换,现在使用的是位置:

Fn Snake_Move(键盘_输入:分辨率<;输入<;keycode>;,mut head_Position:查询<;(&;蛇头,&;mut位置)>;,){for(_head,mut pos)in&;mut head_position tions.iter(){if keyboard_input.press(keycode::Left){pos.x-=1;}if keyboard_input.press。}if keyboard_input.press(keycode::down){pos.y-=1;}if keyboard_input.press(keycode::up){pos.y+=1;}}。

在上一步中你会看到一条被压扁的蛇,是因为默认的双窗口大小不是正方形,但是我们的网格是正方形的,所以我们网格中的每个坐标都比它的高要宽。有一个简单的解决办法,那就是在构建应用程序时创建一个WindowDescriptor资源:

App::build().add_resource(WindowDescriptor{//<;--Title:";.to_string(),//<;--width:2000,//<;--Height:2000,//<;--..Default::Default()//<;--}).add_start_system(setup.system())

现在,让我们更改清晰的颜色(也就是背景色),让它看起来更漂亮一些,插入以下USE语句以获得ClearColorstruct:

现在我们又回到了正方形,现在背景更暗了:

现在我们已经让蛇动了一会儿,让我们给它吃点东西吧。我们将首先在Materials结构中添加一个新的Food_Material字段:

我们需要创建计时器的持续时间,我们需要随机性,这样我们就可以在随机的地点放置食物,所以让我们提前使用这些:

然后,我们将引入两个新的结构;一个Food组件,它让我们知道哪些实体是食物;另一个定时器会间歇性地触发,告诉我们产生一些食物:

Struct Food;struct FoodSpawnTimer(Timer);Iml Default for FoodSpawnTimer{fn Default()->;self{self(Timer::New(Duration::From_Millis(1000),true))}}。

当我解释以下新系统时,创建默认设置的原因将变得清晰(希望如此):

Fn Food_spawner(mut Commands:Commands,Material:res<;Materials>;,time:res<;time>;,mut Timer:Local<;FoodSpawnTimer>;,){Timer。0.tick(time.Delta_sec);如果计时器。0.fined{Commands.spwn(SpriteComponents{Material:Material_Material(),.Default::Default()}).with(Food).with(Position{x:(Random::<;F32>;()*Arena_Width as F32)as I32,y:(Random::<;F32>;()*Arena_Height as F32)as I32,}).with(Size::Square()*Arena_Height as F32)as I32,}).with(Size::Square()*Arena_Height as F32)as I32,}

我们通过定时器参数引入了本地资源的概念。Bevy将看到这个参数,并使用我们的默认实现实例化FoodSpawnTimer类型的值。这将在系统第一次运行时发生,之后它将重复使用相同的计时器。以这种方式使用本地资源可能比手动注册资源更符合人体工程学。计时器在重复,所以我们只要不停地呼叫Tick,每当系统运行完毕时,我们就会随机地典当一些食物。以下是现在的情况:

我们要解决蛇的运动问题。具体地说,无论我们当前是否按下任何键,我们都希望蛇移动,并且我们希望它每X秒移动一次,而不是每帧移动一次。我们将在相当多的领域进行更改,因此,如果您不确定某些内容的发展方向,请选中上面的比较按钮。

#[派生(PartialEq,Copy,Clone)]枚举方向{Left,Up,Right,Down,}实施方向{fn Reverse(Self)->;Self{匹配Self{Self::Left=>;Self::Right,Self::Right=>;Self::Left,Self::Up=>;Self::Down,Self::Down=>;Self::Up,}。

我们将把这个方向添加到蛇头结构中,这样它就知道它在往哪个方向走:

我们现在必须用方向实例化蛇头组件,假设它开始上升:

蛇一般不是平滑的,它是一种循序渐进的运动。就像繁殖食物一样,我们将使用计时器让系统每隔X秒/毫秒运行一次。我们将创建一个结构来保存计时器:

我们之所以不把这个计时器作为本地资源来使用,就像食物产卵一样,是因为我们将在几个系统中使用它,所以我将为您省去重构工作。因为我们在几个系统中使用它,所以我们将创建一个新系统来计时:

我们可以把这个定时器逻辑放在蛇形移动系统中,但我喜欢有一个单独的系统的简洁,因为计时器将在多个地方使用。我们只需将该系统添加到应用程序中:

现在,我们可以进入方向逻辑的核心部分,即Snake_movementsystem,以下是更新版本:

Fn Snake_Move(键盘_输入:分辨率<;输入<;keycode>;,SnakeMoveTimer>;,MUT头:查询<;(实体,&;MUT蛇头)>;,MUT位置:查询<;&;MUT位置>;,){if!Snake_Timer;。0.heads.iter_mut(){let mut head_pos=position tions.get_mut(Head_Entity).unwire()中的(head_entity,mut head)的{return;};让dir:Direction=if keyboard_input.press(keycode::Left){Direction::Left}Else if keyboard_input.press(keycode::down){Direction::down}Else if keyboard_input.press(keycode::up){Direction::Up}Else if keyboard_input.press(keycode::right){Direction::right}Else{head.Direction.dir};if dir!=head.direction.对立面(){head.Direction=dir。方向{方向::左=>;{head_pos.x-=1;}方向::right=>;{head_pos.x+=1;}方向::up=>;{head_pos.y+=1;}方向::down=>;{head_pos.y-=1;}}。

这里没有太多的新概念,只是游戏逻辑。在这之后,你会有一个蛇头,它移动得更多一点…。蛇形的:

蛇的尾巴有点复杂。对于每个细分市场,我们都需要知道它下一步需要去哪里。我们处理这一问题的方法是将这些蛇片段放在VEC中,并将其作为资源存储。这样,当我们更新线段的位置时,我们可以遍历所有的线段,并将每个线段的位置设置为它之前的线段的位置。

命令.INSERT_RESOURCE(Materials{head_Material:Material s.add(Color::RGB(0.7,0.7,0.7).into()),Segment_Material:Material s.add(Color::RGB(0.3,0.3,0.3).into()),//<;--Food_Material:Material s.add(Color::RGB(1.0,0.0,1.0).into(),});

由于我们将从几个地方(当您吃食物和初始化蛇时)生成片段,因此我们将创建一个辅助函数:

FN SPOWN_SEGMENT(命令:&;mut Commands,Material:&;Handle<;ColorMaterial>;,Position:Position)->;Entity{Commands.spwn(SpriteComponents{Material:Material.clone(),..SpriteComponents::Default()}).with(SnakeSegment).with(Position).with(Size::Square(0.65));Commands.Current_Entity().unWrap。

这看起来应该与蛇头的产卵非常相似,但它有一个SnakeSegment组件,而不是蛇头组件。这里的一些新功能是,我们通过使用CURRENT_ENTITY函数获取该实体(实际上只是一个id),并返回它以便调用者可以使用它。现在,我们需要修改我们的游戏设置功能。它不只是一个头,它还会产卵出…。一段蛇片段(震惊的皮卡丘表情包):

FN GAME_SETUP(mut Commands:Commands,Material:res<;Materials>;,mut Segments:ResMut;SnakeSegments>;,){let First_Segment=SPOWN_SEGMENT(&;mut命令,&;Material。Segment_Material,Position{x:3,y:2},);Segments(&;mut Commands,&;Material,Position{x:3,y:2},);Segments。0=vec![First_Segment];Commands.spwn(SpriteComponents{Material:Materials.head_Material al.clone(),Sprite:Sprite::New(ve2::New(10.0,10.0)),.Default::Default()}).with(蛇头{Direction:Direction::Up,}).with(Position{x:3,y:3}).with(大小:正方形(0.8));}。

我们使用SPOWN_SEGMENT函数创建一个带有SnakeSegment组件的实体。然后,我们获取返回的实体,并将SnakeSegments资源设置为具有单个元素的向量,即新创建的片段。瞧,我们有一条分离的小“尾巴”:

在我的记忆中,蛇游戏的一个重要部分是头部不会立即从尾巴上分离出来。让我们看看如何修改Snake_Move函数,使其更贴近原始游戏。我们需要做的第一件事是将SnakeSegments资源添加到Snake_movement函数:

Fn Snake_Move(键盘_输入:分辨率<;输入<;keycode>;,SnakeMoveTimer>;,段:ResMut;SnakeSegments>;,//<;--MUT头:查询<;(实体,&;MUT蛇头)>;,MUT位置:查询<;&;

现在,就在比赛之前,我们将复制head_pos变量,因为我们需要知道头部移动之前的位置:

我们需要这样做的原因是,我们对位置组件的查询返回一个可变位置。Bevy阻止我们随意借用相同成分的倍数,因为如果你能在两个地方变异相同的位置,就会发生时髦的事情。在下降之后,我们将添加我们的逻辑更新细分市场位置:

让mut Segment_Position:VEC<;Position>;=Segments。0.iter().map(|e|*position tions.get_mut(*e).unwire()).Collect::<;vec<;position>;>;();Segment_Positions.Insert(0,last_head_pos);Segment_Positions.iter().zip(段。0.iter().for_each(|(pos,Segment)|{*position tions.get_mut(*Segment).unwire()=*pos;});

基本上,我们得到当前的部门位置,然后添加之前的头部位置。现在我们已经有了分段应该到达的位置的列表。第一个片段将到达头部的位置,第二个片段将到达第一个片段的位置,第三个片段将到达第二个片段的位置,依此类推。

这条蛇被不能吃的食物嘲弄已经够久了。我们将增加一个让蛇吃东西的新系统:

Fn Snake_Eating(MUT命令:命令,SnakeTimer:ResMut<;SnakeMoveTimer&>,MUT Growth_Events:ResMut<;Events<;GrowthEvent>;,Food_Position:Query<;with<;Food,(Entity,&;Position)>;,Head_Position:Query<;with<;Snakehead,&。0.head_position tions.iter(){for(ent,Food_pos)in Food_postions.iter(){if Food_pos==head_pos{Commands.despawn(Ent);growth_events.end(GrowthEvent);}。

只需重复所有的食物位置,看看它们是否与蛇头的位置相同。如果有,我们使用h删除它们。

.