Skip to content

Latest commit

 

History

History
1097 lines (714 loc) · 60.7 KB

File metadata and controls

1097 lines (714 loc) · 60.7 KB

六、碰撞物体

概观

在这一章中,我们将通过给我们的游戏添加更多的机制和对象来继续我们在上一章中介绍的基于碰撞的游戏。首先,我们将从上一章开始,介绍对象碰撞。您将学习如何使用碰撞盒、碰撞触发器、重叠事件、碰撞事件和物理模拟。您还将学习如何使用计时器、投射物运动组件和物理材料。

简介

在前一章中,我们遇到了碰撞的一些基本概念,即线条痕迹和扫掠痕迹。我们学习了如何执行不同类型的线跟踪,如何创建自己的自定义跟踪通道,以及如何更改对象对特定通道的响应方式。您在上一章中学到的许多东西将在本章中使用,我们将在本章中了解对象碰撞。

在本章中,我们将继续通过添加围绕物体碰撞的游戏机制来建立自上而下的Dodgeball游戏。我们将创建躲避球演员,它将充当从地板和墙壁上反弹的躲避球;一个墙演员,会遮挡所有物体;一个鬼壁演员,只会挡住玩家,不会挡住敌人的视线或者躲避球;以及胜利箱演员,当玩家进入胜利箱时将结束游戏,代表关卡结束。

在开始创建我们的Dodgeball类之前,我们将在下一节复习对象碰撞的基本概念。

UE4 中的物体碰撞

每个游戏开发工具都必须有一个物理引擎来模拟多个对象之间的碰撞,如前一章所述。碰撞是如今发布的大多数游戏的支柱,无论是 2D 还是 3D。在许多游戏中,这是玩家作用于环境的主要方式,无论是奔跑、跳跃还是射击,环境都会通过让玩家着陆、被击中等方式做出相应的反应。毫不夸张地说,如果没有模拟碰撞,根本不可能制作很多游戏。

因此,让我们从碰撞组件开始,了解物体碰撞在 UE4 中是如何工作的,以及我们可以使用它的方式。

碰撞部件

在 UE4 中,有两种类型的组件可以影响和被影响碰撞;它们如下:

  • 网状物
  • 形状对象

网格可以像立方体一样简单,也可以像拥有数万个顶点的高分辨率角色一样复杂。网格的碰撞可以通过在网格旁边导入到 UE4 中的自定义文件来指定(这不在本书的范围内),也可以由 UE4 自动计算并由您自定义。

通常,保持碰撞网格尽可能简单(几个三角形)是一个好的做法,这样物理引擎可以在运行时有效地计算碰撞。可能发生碰撞的网格类型如下:

  • 静态网格
  • 骨骼网格
  • 程序网格
  • 等等

形状对象,是以线框模式表示的简单网格,用于通过引起和接收碰撞事件来表现为碰撞对象。

注意

线框模式是游戏开发中常用的可视化模式,通常用于调试目的,它允许您看到没有任何面或纹理的网格–只能通过它们的边看到它们,这些边由它们的顶点连接。当我们向演员添加一个形状组件时,你会看到什么是线框模式。

请注意,形状对象本质上是不可见的网格,它们的三种类型如下:

  • 盒子碰撞(C++ 中的盒子组件)

  • 球体碰撞(C++ 中的球体组件)

  • Capsule Collider (Capsule Component in C++)

    注意

    有一个类,所有提供几何和碰撞的组件都继承自这个类,这就是Primitive组件。该组件是包含任何几何图形的所有组件的基础,网格组件和形状组件就是这种情况。

那么,这些组件如何碰撞,当它们碰撞时会发生什么?我们将在下一节“碰撞事件”中看到这一点。

碰撞事件

假设有两个物体相互碰撞。可能会发生两件事:

  • 它们相互重叠,就好像另一个对象不在那里,在这种情况下,调用Overlap事件。
  • 它们相互碰撞并阻止对方继续前进,在这种情况下Block事件被称为。

在前一章中,我们学习了如何改变对象对特定Trace通道的响应。在这个过程中,我们了解到物体的反应可以是BlockOverlapIgnore

现在,让我们看看在碰撞过程中,这些反应会发生什么。

阻挡:两个物体只有对另一个物体的反应都设置为Block时才会互相阻挡;

  • 两个对象的OnHit事件都将被调用。每当两个物体在碰撞时挡住彼此的路径时,就会调用此事件。如果其中一个对象正在模拟物理,该对象必须将其SimulationGeneratesHitEvents属性设置为true
  • 两个物体会物理地阻止彼此继续它们的进程。

请看下图,该图显示了两个对象被抛出并相互弹开的示例:

Figure 6.1: Object A and Object B blocking each other

图 6.1:对象 A 和对象 B 相互阻塞

重叠:两个物体如果不相互遮挡,也没有一个物体忽略另一个物体,两个物体就会重叠;

  • 如果两个对象的GenerateOverlapEvents属性都设置为true,那么它们的OnBeginOverlapOnEndOverlap事件将被调用。当一个对象开始和停止与另一个对象重叠时,分别调用这些重叠事件。如果其中至少有一个没有将此属性设置为true,则它们都不会调用这些事件。
  • 这些对象的行为就像另一个对象不存在一样,并且会相互重叠。

举个例子,假设玩家的角色走进一个标志关卡结束的触发框,这个触发框只对玩家的角色做出反应。

请看下图,该图显示了两个对象相互重叠的示例:

Figure 6.2: Object A and Object B overlapping each other

图 6.2:对象 A 和对象 B 相互重叠

忽略:如果两个对象中至少有一个忽略了另一个,那么这两个对象就会互相忽略:

  • 两个对象上都不会调用任何事件。
  • 类似于Overlap响应,对象会表现得好像另一个对象不存在一样,并且会相互重叠。

两个对象相互忽略的一个例子是,当玩家角色以外的对象进入标志关卡结束的触发框时,触发框只对玩家角色做出反应。

注意

可以看看上图,两个物体相互重叠的地方,了解忽略

下面的表格有助于您理解两个对象必须具有的必要响应,以便触发前面描述的情况:

Figure 6.3: Resulting responses on objects based on Block, Overlap, and Ignore

图 6.3:基于块、重叠和忽略的对象响应结果

根据此表,假设您有两个对象-对象 A 和对象 B:

  • 如果对象 A 将其对对象 B 的响应设置为Block,而对象 B 将其对对象 A 的响应设置为Block,它们将相互Block

  • 如果对象 A 将其对对象 B 的响应设置为Block,而对象 B 将其对对象 A 的响应设置为Overlap,它们将相互Overlap

  • If Object A has set its response to Object B to Ignore and Object B has set its response to Object A to Overlap, they will Ignore each other.

    注意

    你可以在这里找到 UE4 碰撞交互的完整参考资料:https://docs . unrealengine . com/en-US/Engine/Physics/conflict/Overview

物体之间的碰撞有两个方面:

【时间】物理学【时间】T1:所有与物理模拟相关的碰撞,例如球受重力影响而从地板和墙壁上反弹。

游戏内碰撞的物理模拟响应,可以是:

  • 两个物体继续它们的轨迹,好像另一个物体不在那里(没有物理碰撞)。
  • 两个物体碰撞并改变它们的轨迹,通常其中至少有一个物体继续运动,也就是说,阻挡了彼此的路径。

查询:查询可以分为两个方面的碰撞,如下:

  • 与游戏调用的对象碰撞相关的事件,您可以使用这些事件来创建附加逻辑。这些事件与我们之前提到的相同:
  • OnHit事件
  • OnBeginOverlap事件
  • OnEndOverlap事件
  • 游戏内碰撞的物理反应,可以是:
  • 两个物体继续运动,就好像另一个物体不在那里一样(没有物理碰撞)
  • 两个物体相互碰撞并阻挡对方的道路

物理方面的物理响应听起来可能类似于查询方面的物理响应;然而,尽管这些都是物理反应,但它们会导致对象行为不同。

“物理”方面的物理响应(物理模拟)仅适用于对象模拟物理时(例如,受重力影响、从墙壁和地面反弹等)。例如,这样的物体,当碰到墙壁时,会反弹回来,继续向另一个方向移动。

另一方面,来自查询方面的物理响应适用于所有不模拟物理的对象。当由代码控制时(例如,通过使用SetActorLocation功能或通过使用角色移动组件),对象可以在不模拟物理的情况下移动。在这种情况下,根据您使用的移动对象及其属性的方法,当对象碰到墙壁时,它将简单地停止移动,而不是弹回。这是因为你只是告诉物体向某个方向移动,而有东西挡住了它的路径,所以物理引擎不允许那个物体继续移动。

在下一节中,我们将研究碰撞通道。

碰撞通道

在前一章,我们看了一下现有的 Trace channel(可见度**相机)并学习了如何制作自己的定制通道。现在,您已经了解了跟踪通道,现在是时候讨论对象通道了,也称为对象类型。

虽然跟踪通道仅用于线跟踪,但对象通道用于对象碰撞。您可以为每个Object通道指定一个“目的”,就像跟踪通道一样,如棋子、静态对象、物理对象、投射体等。然后,您可以指定希望每个对象类型如何通过阻止、重叠或忽略该类型的对象来响应所有其他对象类型。

碰撞属性

现在我们已经了解了碰撞是如何工作的,让我们回到上一章中选择的立方体的碰撞设置,在那里我们更改了它对可见性通道的响应。

这个立方体可以在下面的截图中看到:

Figure 6.4: Cube blocking the SightSource of the enemy

图 6.4:立方体挡住敌人的视线来源

在编辑器中打开级别,选择立方体并进入其详细信息面板的Collision部分:

Figure 6.5: The changes in the level editor

图 6.5:级别编辑器中的变化

在这里,我们可以看到一些对我们很重要的选项:

  • SimulationGeneratesHitEvents,当物体模拟物理时,允许调用OnHit事件(我们将在本章后面讨论)。
  • GenerateOverlapEvents,允许调用OnBeginOverlapOnEndOverlap事件。
  • CanCharacterStepUpOn,可以让角色轻松登上这个物体。
  • CollisionPresets,允许我们指定该对象如何响应每个碰撞通道。

让我们将CollisionPresets值从Default更改为Custom,并查看显示的新选项:

Figure 6.6: Changes in Collision Presets

图 6.6:碰撞预设的变化

这些选项中的第一个是CollisionEnabled属性。它允许您指定要考虑碰撞的哪些方面:查询、物理、两者或无。同样,物理碰撞与物理模拟有关(该对象是否会被模拟物理的其他对象考虑),而查询碰撞与碰撞事件以及对象是否会阻止彼此的移动有关:

Figure 6.7: Collision Enabled for Query and Physics

图 6.7:为查询和物理启用冲突

第二个选项是ObjectType属性。这与跟踪通道概念非常相似,但专门用于对象碰撞,最重要的是,它规定了这是什么类型的碰撞对象。UE4 附带的对象类型值如下:

  • WorldStatic:不动的物体(建筑物、构筑物等)
  • WorldDynamic:可以移动的物体(由代码触发移动的物体,玩家可以拾取移动的物体,等等)
  • Pawn:用于可以在关卡中控制和移动的棋子
  • PhysicsBody:用于模拟物理的物体
  • Vehicle:用于车辆对象
  • Destructible:用于可破坏的网格

如前所述,您也可以创建自己的自定义对象类型(这将在本章后面提到),类似于您可以如何创建自己的跟踪通道(,这在上一章中有介绍)。

我们最后的选择与Collision Responses有关。假定这个Cube对象有默认的碰撞选项,所有的响应都被设置为Block,这意味着这个对象将阻挡所有的线轨迹和所有阻挡WorldStatic对象的对象,假定这是这个对象的类型。

由于碰撞属性有如此多的不同组合,UE4 允许您以碰撞预设的形式对碰撞属性值进行分组。

回到CollisionPresets属性,当前设置为Custom点击,可以看到所有可能的选项。现有的一些Collision Presets如下:

无碰撞:用于不受任何碰撞影响的物体:

  • Collision Enabled : NoCollision
  • Object Type : WorldStatic
  • 答复:不相关
  • 示例:纯视觉和远距离的对象,例如玩家永远无法触及的对象

阻挡所有:用于静止的物体,阻挡所有其他物体:

  • Collision Enabled : QueryPhysics
  • Object Type : WorldStatic
  • 响应:Block所有通道
  • 示例:靠近玩家角色并阻止其移动的对象,例如地板和墙壁,它们将始终保持静止

全部重叠:用于静态对象,并与所有其他对象重叠:

  • Collision Enabled:仅限Query
  • Object Type : WorldStatic
  • 响应:Overlap所有通道
  • 示例:放置在关卡中的触发框,该关卡将始终保持静止

阻挡所有动态:类似于Block All预设,但是对于在游戏过程中可能改变变换的动态对象(Object Type : WorldDynamic)

重叠所有动态:类似于Overlap All预设,但是对于在游戏过程中可能改变变换的动态对象(Object Type : WorldDynamic)

棋子:用于棋子和人物:

  • Collision Enabled : QueryPhysics
  • Object Type : Pawn
  • 响应:Block所有通道,Ignore能见度通道
  • 示例:玩家角色和不可玩角色

物理演员:用于模拟物理的物体:

  • Collision Enabled : QueryPhysics
  • Object Type : PhysicsBody
  • 响应:Block所有通道
  • 示例:受物理影响的对象,例如从地板和墙壁反弹的球

就像其他碰撞属性一样,您也可以创建自己的碰撞预设。

注意

你可以在这里找到 UE4 碰撞响应的完整参考资料:https://docs . unrealengine . com/en-US/Engine/Physics/conflict/Reference

现在我们已经了解了碰撞的基本概念,让我们开始创建Dodgeball类。下一个练习将引导你去做。

练习 6.01:创建躲避球课

在本练习中,我们将创建我们的Dodgeball类,它将被我们的敌人投掷并从地板和墙壁上反弹,就像一个真正的躲避球一样。

在我们真正开始创建Dodgeball C++ 类及其逻辑之前,我们应该为它设置所有必要的冲突设置。

以下步骤将帮助您完成本练习:

  1. 打开我们的Project Settings,进入Engine部分的Collision小节。目前没有对象通道,所以您需要创建一个新的。

  2. 按下New Object Channel按钮,命名为Dodgeball,将其Default Response设置为Block

  3. 完成后,展开Preset部分。在这里,你可以找到 UE4 中所有可用的默认预设。如果您选择其中一个并按下Edit选项,您可以更改Preset碰撞的设置。

  4. 通过按下New选项创建您自己的Preset。我们希望我们的Dodgeball Preset设置如下:

    • Name : Dodgeball
    • CollisionEnabled : Collision Enabled (Query and Physics)(我们希望在物理模拟和碰撞事件中考虑这一点)
    • Object Type : Dodgeball
    • Collision Responses:大部分选项选择遮挡,但是忽略摄像头和EnemySight(我们不希望躲避球遮挡摄像头或者敌人的视线)
  5. Once you've selected the correct options, press Accept.

    现在Dodgeball类的碰撞设置已经设置好了,让我们创建Dodgeball C++ 类。

  6. Content Browser内,右键点击,选择New C++ Class

  7. 选择Actor作为父类。

  8. 选择DodgeballProjectile作为类的名称(我们的项目已经命名为Dodgeball,所以我们不能把这个新类也命名为那个)。

  9. 在 Visual Studio 中打开DodgeballProjectile类文件。我们要做的第一件事是添加躲避球的碰撞组件,所以我们将在我们的类头中添加一个SphereComponent(演员组件属性通常是私有的 ):

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   Dodgeball, meta = (AllowPrivateAccess = "true"))
    class USphereComponent* SphereComponent;
  10. Next, include the SphereComponent class at the top of our source file:

```cpp
#include "Components/SphereComponent.h"
```

注意

请记住,所有头文件包含必须在. generated.h include 之前。

现在,转到`DodgeballProjectile`类的构造函数,在其源文件中,并执行以下步骤。
  1. 创建SphereComponent对象:
```cpp
SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere   Collision"));
```
  1. 将其radius设置为35单位:
```cpp
SphereComponent->SetSphereRadius(35.f);
```
  1. 将其Collision Preset设置为我们创建的Dodgeball预设:
```cpp
SphereComponent->SetCollisionProfileName(FName("Dodgeball"));
```
  1. 我们希望Dodgeball模拟物理,所以通知这个组件,如下面的代码片段所示:
```cpp
SphereComponent->SetSimulatePhysics(true);
```
  1. We want the Dodgeball to call the OnHit event while simulating physics, so call the SetNotifyRigidBodyCollision function in order to set that to true (this is the same as the SimulationGeneratesHitEvents property that we saw in the Collision section of an object's properties):
```cpp
//Simulation generates Hit events
SphereComponent->SetNotifyRigidBodyCollision(true);
```

我们也会想听听`SphereComponent`的`OnHit`事件。
  1. DodgeballProjectile类的头文件中为OnHit事件被触发时将被调用的函数创建一个声明。这个功能应该叫OnHit。应该是public,什么都不返回(void),有UFUNCTION宏,接收一些参数,按照这个顺序: * UPrimitiveComponent* HitComp:被击中的属于这个演员的部件。基本组件是具有Transform属性和某种几何图形的参与者组件(例如,MeshShape组件)。 * AActor* OtherActor:碰撞中涉及的另一个演员。 * UPrimitiveComponent* OtherComp:被击中的属于对方演员的部件。 * FVector NormalImpulse:物体被击中后移动的方向,以及用多大的力(通过检查矢量的大小)。此参数仅对模拟物理的对象非零。 * FHitResult& Hit: The data of the Hit resulting from the collision between this object and the other object. As we saw in the previous chapter, it contains properties such as the location of the Hit, its normal, which component and actor it hit, and so on. Most of the relevant information is already available to us through the other parameters, but if you need more detailed information, you can access this parameter:

    UFUNCTION()
    void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,   UPrimitiveComponent* OtherComp, FVector NormalImpulse, const   FHitResult& Hit);

    OnHit函数的实现添加到类的源文件中,并且在该函数中,至少在目前,当躲避球击中玩家时摧毁它。

  2. Cast the OtherActor parameter to our DodgeballCharacter class and check if the value is not a nullptr. If it's not, which means that the other actor we hit is a DodgeballCharacter, we'll destroy this DodgeballProjectile actor:

```cpp
void ADodgeballProjectile::OnHit(UPrimitiveComponent *   HitComp, AActor * OtherActor, UPrimitiveComponent *   OtherComp, FVector NormalImpulse, const FHitResult & Hit)
{
  if (Cast<ADodgeballCharacter>(OtherActor) != nullptr)
  {
    Destroy();
  }
}
```

假设我们引用的是`DodgebalCharacter`类,我们需要将它包含在这个类的源文件的顶部:

```cpp
#include "DodgeballCharacter.h"
```

注意

在下一章中,我们将改变这个功能,这样我们就可以让躲避球在摧毁自己之前先伤害玩家。当我们谈论演员组件时,我们将这样做。
  1. Head back to the DodgeballProjectile class's constructor and add the following line at the end in order to listen to the OnHit event of SphereComponent:
```cpp
// Listen to the OnComponentHit event by binding it to our   function
SphereComponent->OnComponentHit.AddDynamic(this,   &ADodgeballProjectile::OnHit);
```

这将把我们创建的`OnHit`函数绑定到这个`SphereComponent` `OnHit`事件(因为这是一个演员组件,这个事件被称为`OnComponentHit`,这意味着我们的函数将与那个事件一起被调用。
  1. Lastly, make SphereComponent this actor's RootComponent, as shown in the following code snippet:
```cpp
// Set this Sphere Component as the root component,
// otherwise collision won't behave properly
RootComponent = SphereComponent;
```

注意

为了让一个移动的演员在碰撞中表现正确,无论是否模拟物理,演员的主要碰撞成分通常必须是它的`RootComponent`。

例如,`Character`类的`RootComponent`是一个胶囊碰撞器组件,因为那个演员会四处移动,而那个组件是角色与环境碰撞的主要方式。

现在我们已经添加了`DodgeballProjectile` C++ 类的逻辑,让我们继续创建我们的蓝图类。
  1. 编译您的更改并打开编辑器。
  2. 进入内容浏览器中的Content > ThirdPersonCPP > Blueprints目录,右键点击,新建一个蓝图类。
  3. 展开All Classes部分,搜索DodgeballProjectile类,然后将其设置为父类。
  4. 命名新蓝图类BP_DodgeballProjectile
  5. 打开这个新的蓝图类。
  6. Notice the wireframe representation of the SphereCollision component in the actor's Viewport window (this is hidden by default during the game, but you can change that property in this component's Rendering section by changing its HiddenInGame property):
![Figure 6.8: Visual wireframe representation of the SphereCollision component ](img/B16183_06_08.jpg)

图 6.8:球体生态网格组件的可视化线框表示
  1. Now, add a new Sphere mesh as a child of the existing Sphere Collision component:
![Figure 6.9: Adding a Sphere mesh ](img/B16183_06_09.jpg)

图 6.9:添加球体网格
  1. Change its scale to 0.65, as shown in the following screenshot:
![Figure 6.10: Updating the scale ](img/B16183_06_10.jpg)

图 6.10:更新比例
  1. Set its Collision Presets to NoCollision:
![Figure 6.11: Updating Collision Presets to NoCollision ](img/B16183_06_11.jpg)

图 6.11:将碰撞预设更新为无碰撞
  1. Finally, open our level and place an instance of the BP_DodgeballProjectile class near the player (this one was placed at a height of 600 units):
![Figure 6.12: Dodgeball bouncing on the ground ](img/B16183_06_12.jpg)

图 6.12:躲避球在地上弹跳

做完这些后,再玩关卡。你会注意到躲避球会受到重力的影响,在静止之前会离开地面几次。

通过完成本练习,您已经创建了一个行为类似于物理对象的对象。

现在,您知道如何创建自己的碰撞对象类型,使用OnHit事件,以及更改对象的碰撞属性。

注意

上一章我们简单提到了LineTraceSingleByObjectType。现在我们已经知道了对象碰撞是如何工作的,我们可以简单地提到它的用途:当执行一个检查跟踪通道的线跟踪时,您应该使用LineTraceSingleByChannel功能;当执行检查Object通道(对象类型)的线跟踪时,您应该使用LineTraceSingleByObjectType功能。应该明确的是,与LineTraceSingleByChannel函数不同,该函数不会检查阻挡特定对象类型的对象,而是检查那些特定对象类型的对象。这两个函数具有完全相同的参数,并且跟踪通道和对象通道都可以通过ECollisionChannel枚举获得。

但是如果你想让球从地板上反弹更多次呢?如果你想让它更有弹性呢?这就是物理材料的来源。

物理材料

在 UE4 中,您可以通过“物理材料”自定义对象在模拟物理时的行为方式。为了进入这种新型资产,让我们创建自己的:

  1. Content文件夹内创建一个名为Physics的新文件夹。

  2. 在该文件夹中右键单击Content Browser上的,在Create Advanced Asset部分下,转到Physics子部分并选择Physical Material

  3. 命名这种新的物理材料PM_Dodgeball

  4. Open the asset and take a look at the available options.

    Figure 6.13: Asset options

图 6.13:资产选项

我们应该注意的主要选项如下:

  • Friction:这个属性从01指定摩擦力会对这个物体产生多大的影响(0表示这个物体会像在冰上一样滑动,1表示这个物体会像一块口香糖一样粘在一起)。
  • Restitution(也称弹跳):该属性从01并指定与另一个物体碰撞后将保持多少速度(0表示该物体永远不会从地面反弹,而1表示该物体将长时间反弹)。
  • Density:此属性指定该对象的密度(即相对于其网格的重量)。两个物体的大小可以相同,但是如果一个物体的密度是另一个物体的两倍,那就意味着它的重量是另一个物体的两倍。

为了让我们的DodgeballProjectile对象表现得更接近真实的躲避球,它必须承受相当大的摩擦(默认值是0.7,这已经足够高了)并且非常有弹性。让我们将该物理材料的Restitution属性增加到0.95

完成此操作后,打开BP_DodgeballProjectile蓝图类,并将球体碰撞组件在其Collision部分内的物理材质更改为我们刚刚创建的材质PM_Dodgeball:

Figure 6.14: Updating the BP_DodgeballProjectile Blueprint class

图 6.14:更新 BP _ 躲避球投射蓝图类

注意

确保你添加到关卡中的躲避球演员的实例也有这个物理材料。

如果你再玩一次我们在练习 6.01创建躲避球类中创建的关卡,你会注意到我们的BP_DodgeballProjectile现在会在静止之前从地面反弹几次,表现得更像一个真正的躲避球。

做了所有这些,我们只是错过了一件事,让我们的Dodgeball演员表现得像一个真正的躲避球。现在,我们没有办法扔掉它。因此,让我们通过创建一个投射物运动组件来解决这个问题,这是我们将在下一个练习中做的事情。

在前面的章节中,当我们复制第三人称模板项目时,我们了解到 UE4 附带的Character类有一个CharacterMovementComponent。这个 actor 组件允许一个 actor 以各种方式在级别中移动,并且有许多属性允许您根据自己的偏好对其进行自定义。然而,还有另一个同样常用的运动部件:ProjectileMovementComponent

ProjectileMovementComponent actor 组件用于将投射物的行为归于一个 actor。它允许你设置初始速度,重力,甚至一些物理模拟参数,如BouncinessFriction。然而,鉴于我们的Dodgeball Projectile已经在模拟物理,我们将使用的唯一属性是InitialSpeed

练习 6.02:向躲避球投射体添加投射体移动组件

在本练习中,我们将在DodgeballProjectile上添加一个ProjectileMovementComponent,使其具有初始水平速度。我们这样做是为了让它能被我们的敌人抛出,而不只是垂直落下。

以下步骤将帮助您完成本练习:

  1. DodgeballProjectile类的头文件添加一个ProjectileMovementComponent属性:

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   Dodgeball, meta = (AllowPrivateAccess = "true"))
    class UProjectileMovementComponent* ProjectileMovement;
  2. 在类的源文件顶部包含ProjectileMovementComponent类:

    #include "GameFramework/ProjectileMovementComponent.h"
  3. 在类构造函数的末尾,创建ProjectileMovementComponent对象:

    ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Pro   jectile Movement"));
  4. 然后,将其InitialSpeed设置为1500单位:

    ProjectileMovement->InitialSpeed = 1500.f;

完成后,编译您的项目并打开编辑器。为了演示躲避球的初始速度,降低其在 Z 轴上的位置,并将其放在玩家后面(这一个被放置在 200 单位的高度):

Figure 6.15: Dodgeball moving along the X axis

图 6.15:躲避球沿 X 轴移动

当你玩关卡时,你会注意到躲避球开始向它的 X 轴(红色箭头)移动:

这样,我们就可以结束我们的练习了。我们的DodgeballProjectile现在表现得像一个真正的躲避球。它落下,反弹,然后被抛出。

我们项目的下一步将是给我们的EnemyCharacter添加逻辑,这样它就可以向玩家扔躲避球,但是在我们解决这个问题之前,我们必须解决计时器的概念。

计时器

考虑到电子游戏的性质和它们强烈基于事件的事实,每个游戏开发工具都必须有一种方法让你在事情发生之前造成延迟或等待时间。例如,当你玩在线死亡匹配游戏时,你的角色可以死亡然后重生,通常,重生事件不会在你的角色死亡的瞬间发生,而是在几秒钟后。有许多情况下,你希望某件事发生,但只是在一定时间后。我们的EnemyCharacter将会是这种情况,每隔几秒就会投掷闪避球。这种延迟或等待时间可以通过定时器来实现。

一个定时器允许你在一定时间后调用一个函数。您可以选择以一个时间间隔循环函数调用,也可以在循环开始前设置一个延迟。如果你想让计时器停止,你也可以这样做。

我们将使用计时器,以便我们的敌人每X时间扔一个闪避球,无限期地,只要它能看到玩家角色,然后当敌人不能再看到它的目标时停止那个计时器。

在我们开始给我们的EnemyCharacter类添加逻辑,让它向玩家扔闪避球之前,我们应该看一下另一个话题,那就是如何催生演员。

产卵演员

第 1 章**虚幻引擎介绍中,你通过编辑器学会了如何在关卡中放置你创建的一个演员,但是如果你想在游戏进行的时候将那个演员放置在关卡中呢?这就是我们现在要看的。

与大多数其他游戏开发工具一样,UE4 允许您在游戏本身运行时在游戏中放置一个演员。这个过程叫做产卵。为了在 UE4 中产生一个参与者,我们需要调用SpawnActor函数,该函数可从World对象获得(如前所述,我们可以使用GetWorld函数访问该对象)。但是SpawnActor函数有几个参数需要传递,如下所示:

  • 一个UClass*属性,它让函数知道将要产生的对象的类。该属性可以是 C++ 类,可通过NameOfC++ Class::StaticClass()函数获得,也可以是蓝图类,可通过TSubclassOf属性获得。一般来说,最好不要直接从 C++ 类中派生出参与者,而是创建一个蓝图类并生成它的实例。
  • TSubclassOf属性是一种在 C++ 中引用蓝图类的方式。它用于引用 C++ 代码中的一个类,这个类可能是一个蓝图类。您用模板参数声明了一个TSubclassOf属性,它是类必须继承的 C++ 类。在下一个练习中,我们将了解如何在实践中使用这个属性。
  • 要么是FTransform属性,要么是FVectorFRotator属性,这将指示我们想要生成的对象的位置、旋转和缩放。
  • 一个可选的FActorSpawnParameters属性,允许你指定更多特定于产卵过程的属性,比如谁导致了行为人产卵(也就是Instigator),如果它产卵的位置被其他对象占据了,那么如何处理对象产卵,这可能会导致重叠或者阻塞事件等等。

SpawnActor函数会将一个实例返回给这个函数产生的执行元。假设它也是一个模板函数,您可以这样调用它:您可以直接使用模板参数接收对您生成的参与者类型的引用:

GetWorld()->SpawnActor<NameOfC++ Class>(ClassReference,   SpawnLocation, SpawnRotation);

在这种情况下,SpawnActor函数被调用,在这里我们生成了一个NameOfC++ Class类的实例。这里,我们提供了对具有ClassReference属性的类的引用,以及分别使用SpawnLocationSpawnRotation属性生成的角色的位置和旋转。

您将在练习 6.03向敌人角色中学习如何应用这些属性。

不过,在我们继续练习之前,我想简单地提一下SpawnActor函数的一个变体,它也可能派上用场:SpawnActorDeferred函数。虽然SpawnActor函数将创建您指定的对象的一个实例,然后将其放置在世界中,但这个新的SpawnActorDeferred函数将创建您想要的对象的一个实例,并且仅在您调用演员的FinishSpawning函数时将其放置在世界中。

例如,假设我们想在闪避球产生的那一刻改变它的InitialSpeed。如果我们使用SpawnActor功能,躲避球有可能会在我们设置其InitialSpeed属性之前开始移动。然而,通过使用SpawnActorDeferred功能,我们可以创建一个闪避球的实例,然后将其InitialSpeed设置为我们想要的任何值,并且只有通过调用新创建的闪避球的FinishSpawning功能才能将其放置在世界上,该功能的实例由SpawnActorDeferred功能返回给我们。

现在我们知道了如何在世界上产生一个演员,也知道了计时器的概念,我们可以将负责投掷闪避球的逻辑添加到我们的EnemyCharacter类中,这就是我们在下一个练习中要做的。

练习 6.03:给敌人角色添加投掷逻辑

在本练习中,我们将把负责投掷我们刚刚创建的躲避球演员的逻辑添加到我们的EnemyCharacter类中。

在 Visual Studio 中打开该类的文件以便开始。我们将从修改我们的LookAtActor函数开始,这样我们就可以保存告诉我们是否可以看到玩家的值,并使用它来管理我们的计时器。

按照以下步骤完成本练习:

  1. EnemyCharacter类的头文件中,将LookAtActor函数的返回类型从void改为bool :

    // Change the rotation of the character to face the given   actor
    // Returns whether the given actor can be seen
    bool LookAtActor(AActor* TargetActor);
  2. 在函数的实现中,在类的源文件中做同样的事情,同时在我们调用CanSeeActor函数的if语句的末尾返回true。此外,在第一个if语句中返回false,在这里我们检查TargetActor是否是nullptr并且在函数的末尾:

    bool AEnemyCharacter::LookAtActor(AActor * TargetActor)
    {
      if (TargetActor == nullptr) return false;
      if (CanSeeActor(TargetActor))
      {
        FVector Start = GetActorLocation();
        FVector End = TargetActor->GetActorLocation();
        // Calculate the necessary rotation for the Start point to   face the End point
        FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(Start, End);
        //Set the enemy's rotation to that rotation
        SetActorRotation(LookAtRotation);
        return true;
      }
      return false;
    }
  3. 接下来,添加两个bool属性,bCanSeePlayerbPreviousCanSeePlayer,在你类的头文件中设置为protected,分别代表从敌方角色的角度是否可以在这一帧看到玩家,以及在最后一帧是否可以看到玩家:

    //Whether the enemy can see the player this frame
    bool bCanSeePlayer = false;
    //Whether the enemy could see the player last frame
    bool bPreviousCanSeePlayer = false;
  4. 然后,转到您的类的Tick函数实现,并将bCanSeePlayer的值设置为LookAtActor函数的返回值。这将取代之前对LookAtActor功能的调用:

    // Look at the player character every frame
    bCanSeePlayer = LookAtActor(PlayerCharacter);
  5. 之后,将bPreviousCanSeePlayer的值设置为bCanSeePlayer的值:

    bPreviousCanSeePlayer = bCanSeePlayer;
  6. 在前两行之间,添加一条if语句,检查bCanSeePlayerbPreviousCanSeePlayer的值是否不同。这将意味着要么我们看不到玩家最后一帧,现在我们可以,要么我们可以看到玩家最后一帧,现在我们不能:

    bCanSeePlayer = LookAtActor(PlayerCharacter);
    if (bCanSeePlayer != bPreviousCanSeePlayer)
    {
    }
    bPreviousCanSeePlayer = bCanSeePlayer;
  7. 在这个if语句中,如果我们能看到玩家,我们想启动一个计时器,如果我们不能再看到玩家,我们想停止这个计时器:

    if (bCanSeePlayer != bPreviousCanSeePlayer)
    {
      if (bCanSeePlayer)
      {
        //Start throwing dodgeballs
      }
      else
      {
        //Stop throwing dodgeballs
      }
    }
  8. 为了启动计时器,我们需要在类的头文件中添加以下属性,这些属性都可以是protected:

    • 一个FTimerHandle属性,负责识别我们要启动哪个定时器。它基本上作为一个特定定时器的标识符:

      FTimerHandle ThrowTimerHandle;
    • 一个float属性,表示投掷躲避球之间等待的时间(间隔),这样我们就可以循环计时器。我们给它一个默认值2秒:

      float ThrowingInterval = 2.f;
    • 另一个float属性,表示计时器开始循环之前的初始延迟。让我们给它一个默认值0.5秒:

      float ThrowingDelay = 0.5f;
    • A function to be called every time the timer ends, which we will create and call ThrowDodgeball. This function doesn't return anything and doesn't receive any parameters:

      void ThrowDodgeball();

      在我们调用适当的函数来启动计时器之前,我们需要在源文件中为负责的对象FTimerManager添加一个#include

      每个World都有一个定时器管理器,可以启动和停止定时器,并访问与它们相关的功能,如它们是否仍然活跃,运行多长时间等:

      #include "TimerManager.h"
  9. 现在,使用GetWorldTimerManager功能进入当前世界的计时器管理器:

    GetWorldTimerManager()
  10. 接下来,调用定时器管理器的SetTimer功能,如果我们能看到玩家角色,以便启动负责投掷闪避球的定时器。SetTimer功能接收以下参数: * 代表所需计时器的FTimerHandle:ThrowTimerHandle。 * 要调用的函数所属的对象:this。 * 要调用的函数,必须在它的名字前面加上&ClassName::来指定,结果是&AEnemyCharacter::ThrowDodgeball。 * 计时器的速率或间隔:ThrowingInterval。 * 这个计时器是否会循环:true。 * The delay before this timer starts looping: ThrowingDelay.

    以下代码片段包含这些参数:

    if (bCanSeePlayer)
    {
      //Start throwing dodgeballs
      GetWorldTimerManager().SetTimer(ThrowTimerHandle,this,  &AEnemyCharacter::ThrowDodgeball,ThrowingInterval,true,  ThrowingDelay);
    }
  11. If we can no longer see the player and we want to stop the timer, we can do so using the ClearTimer function. This function only needs to receive an FTimerHandle property as a parameter:

```cpp
else
{
  //Stop throwing dodgeballs
  GetWorldTimerManager().ClearTimer(ThrowTimerHandle);
}
```

唯一剩下的就是实现`ThrowDodgeball`功能。该功能将负责产生一个新的`DodgeballProjectile`演员。为了做到这一点,我们需要一个对我们想要生成的类的引用,这个类必须从`DodgeballProjectile`继承,所以接下来我们需要做的是使用`TSubclassOf`对象创建适当的属性。
  1. EnemyCharacter头文件中创建TSubclassOf属性,可以是public :
```cpp
//The class used to spawn a dodgeball object
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   Dodgeball)
TSubclassOf<class ADodgeballProjectile> DodgeballClass;
```
  1. 因为我们将使用DodgeballProjectile类,我们还需要将它包含在EnemyCharacter源文件中:
```cpp
#include "DodgeballProjectile.h"
```
  1. 然后,在源文件中的ThrowDodgeball函数实现中,首先检查该属性是否为nullptr。如果是,我们return立即:
```cpp
void AEnemyCharacter::ThrowDodgeball()
{
  if (DodgeballClass == nullptr)
  {
    return;
  }
}
```
  1. Next, we will be spawning a new actor from that class. Its location will be 40 units in front of the enemy and its rotation will be the same as the enemy. In order to spawn the Dodgeball in front of the enemy character, we'll need to access the enemy's ForwardVector property, which is a unitary FVector (meaning that its length is 1) that indicates the direction an actor is facing, and multiply it by the distance at which we want to spawn our dodgeball, which is 40 units:
```cpp
FVector ForwardVector = GetActorForwardVector();
float SpawnDistance = 40.f;
FVector SpawnLocation = GetActorLocation() + (ForwardVector *   SpawnDistance);
//Spawn new dodgeball
GetWorld()->SpawnActor<ADodgeballProjectile>(DodgeballClass,   SpawnLocation, GetActorRotation());
```

我们需要对`EnemyCharacter`类进行的修改到此结束。在我们设置完这个逻辑的蓝图之前,让我们快速修改一下我们的`DodgeballProjectile`类。
  1. 在 Visual Studio 中打开DodgeballProjectile类的源文件。
  2. Within its BeginPlay event, set its LifeSpan to 5 seconds. This property, which belongs to all actors, dictates how much longer they will remain in the game before being destroyed. By setting our dodgeball's LifeSpan to 5 seconds on its BeginPlay event, we are telling UE4 to destroy that object 5 seconds after it's spawned (or, if it's already been placed in the level, 5 seconds after the game starts). We will do this so that the floor isn't filled with dodge balls after a certain amount of time, which would make the game unintentionally difficult for the player:
```cpp
void ADodgeballProjectile::BeginPlay()
{
  Super::BeginPlay();

  SetLifeSpan(5.f);
}
```

现在我们已经完成了与`EnemyCharacter`类的躲避球投掷逻辑相关的 C++ 逻辑,让我们编译我们的更改,打开编辑器,然后打开我们的`BP_EnemyCharacter`蓝图。在那里,前往`Class Defaults`面板,将`DodgeballClass`房产的价值更改为`BP_DodgeballProjectile`:

![Figure 6.16: Updating the Dodgeball Class ](img/B16183_06_16.jpg)

图 6.16:更新躲避球类

完成此操作后,您可以移除我们在关卡中放置的BP_DodgeballProjectile类的现有实例,如果它仍然存在的话。

现在,我们可以发挥我们的水平。你会注意到,敌人几乎会立即开始向玩家投掷闪避球,并且只要玩家角色在视野中,就会继续这样做:

Figure 6.17: Enemy character throwing dodgeballs if the player is in sight

图 6.17:如果玩家在视线范围内,敌方角色投掷躲避球

至此,我们已经完成了EnemyCharacter的闪避投球逻辑。你现在知道如何使用计时器了,计时器是任何游戏程序员的必备工具。

墙壁

我们项目的下一步是创建Wall类。我们将有两种类型的墙:

  • 一面普通的墙,它会挡住敌人的视线,玩家角色和闪避球。
  • 一面鬼墙,只会挡住玩家角色,无视敌人的视线和闪避球。您可能会在特定类型的益智游戏中发现这种类型的碰撞设置。

在下一个练习中,我们将创建这两个 Wall 类。

练习 6.04:创建墙类

在本练习中,我们将创建代表普通WallGhostWallWall类,它们只会阻挡玩家角色的移动,而不会阻挡敌人的视线或他们投掷的闪避球。

让我们从正常的Wall课开始。这个 C++ 类将基本上是空的,因为它唯一需要的是一个网格,以便反射投射物并阻挡敌人的视线,这将通过它的蓝图类来添加。

以下步骤将帮助您完成本练习:

  1. 打开编辑器。

  2. 在内容浏览器的左上角,按下绿色的Add New按钮。

  3. 选择顶部的第一个选项;Add Feature or Content Pack

  4. 会出现一个新窗口。选择Content Packs标签,然后选择Starter Content包装,然后按下Add To Project按钮。这将向项目中添加一些基本资产,我们将在本章和后面的一些章节中使用这些资产。

  5. 创建一个新的 C++ 类,名为Wall,以Actor类为父类。

  6. 接下来,在 Visual Studio 中打开该类的文件,并添加一个SceneComponent作为我们的 Wall 的RootComponent:

    • Header文件如下:

      private:
      UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Wall,   meta = (AllowPrivateAccess = "true"))
      class USceneComponent* RootScene;
    • Source文件如下:

      AWall::AWall()
      {
        // Set this actor to call Tick() every frame.  You can turn   this off to improve performance if you don't need it.
        PrimaryActorTick.bCanEverTick = true;
        RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
        RootComponent = RootScene;
      }
  7. 编译您的代码并打开编辑器。

  8. 接下来,转到内容浏览器中的Content > ThirdPersonCPP > : Blueprints目录,创建一个继承自Wall类的新蓝图类,将其命名为BP_Wall,并打开该资产。

  9. 添加静态网格组件,并将其StaticMesh属性设置为Wall_400x300

  10. 将其Material属性设置为M_Metal_Steel

  11. Set the Static Mesh Component's location on the X axis to –200 units (so that the mesh is centered relative to our actor's origin):

![Figure 6.18: Updating the Static Mesh Component's location ](img/B16183_06_18.jpg)

图 6.18:更新静态网格组件的位置

这是您的蓝图类的视口应该是什么样子:

Figure 6.19: Blueprint class's Viewport Wall

图 6.19:蓝图类的视口墙

注意

一般来说,当不需要碰撞组件时,最好添加一个SceneComponent作为对象的RootComponent,以便允许其子组件具有更大的灵活性。

演员的RootComponent不能修改其位置或旋转,这就是为什么,在我们的例子中,如果我们在 Wall C++ 类中创建了一个静态网格组件,并将其设置为根组件,而不是使用场景组件,我们将很难偏移它。

现在我们已经建立了常规的Wall类,让我们创建我们的GhostWall类。因为这些类没有任何逻辑设置,我们只是将GhostWall类创建为BP_Wall蓝图类的子类,而不是我们的 C++ 类。

  1. 右键单击BP_Wall资产的,选择Create Child Blueprint Class

  2. 命名新蓝图BP_GhostWall

  3. 打开它。

  4. 更改静态网格组件的碰撞属性:

    • 将其CollisionPreset设置为Custom
    • 将其对EnemySightDodgeball频道的响应更改为Overlap
  5. Change the Static Mesh Component's Material property to M_Metal_Copper.

    你的BP_GhostWall视窗现在应该是这样的:

    Figure 6.20: Creating the Ghost Wall

图 6.20:创建幽灵墙

现在,您已经创建了这两个沃尔演员,请将他们放入关卡中进行测试。将它们的变换设置为以下变换值:

  • 墙:Location : (-710, 120, 130)

  • Ghost Wall: Location: (-910, -100, 130); Rotation: (0, 0, 90):

    Figure 6.21: Updating the Ghost Wall's locations and rotation

图 6.21:更新幽灵墙的位置和旋转

最终结果应该是这样的:

Figure 6.22: Final outcome with the Ghost Wall and the Wall

图 6.22:鬼墙和墙的最终结果

你会注意到,当你将你的角色隐藏在普通Wall(右边的那个)后面时,敌人不会向玩家投掷闪避球;然而,当你试图将你的角色隐藏在GhostWall(左边的那个)后面时,即使敌人无法通过,敌人也会向角色投掷躲避球,他们会像墙不存在一样穿过墙!

我们的练习到此结束。我们制造了我们的Wall演员,他们要么表现正常,要么无视敌人的视线和躲避球!

胜利箱

我们项目的下一步将是创造VictoryBox演员。考虑到玩家已经击败了关卡,当玩家角色进入游戏时,这个演员将负责结束游戏。为了做到这一点,我们将使用Overlap事件。下面的练习将帮助我们理解胜利盒子。

练习 6.05:创建维多利亚盒子类

在本练习中,我们将创建VictoryBox类,当玩家角色输入时,该类将结束游戏。

以下步骤将帮助您完成本练习:

  1. 创建一个从 actor 继承的新 C++ 类,并将其称为VictoryBox

  2. 在 Visual Studio 中打开该类的文件。

  3. 创建一个新的SceneComponent属性,它将被用作RootComponent,就像我们对我们的Wall C++ 类所做的那样:

    • Header文件:

      private:
      UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   VictoryBox, meta = (AllowPrivateAccess = "true"))
      class USceneComponent* RootScene;
    • Source文件:

      AVictoryBox::AVictoryBox()
      {
        // Set this actor to call Tick() every frame.  You can turn   this off to improve performance if you don't need it.
        PrimaryActorTick.bCanEverTick = true;
        RootScene =   CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
        RootComponent = RootScene;
      }
  4. 在头文件中声明一个BoxComponent,用于检查与玩家角色的重叠事件,也应该是private :

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   VictoryBox, meta = (AllowPrivateAccess = "true"))
    class UBoxComponent* CollisionBox;
  5. 在类的源文件中包含BoxComponent文件:

    #include "Components/BoxComponent.h"
  6. 创建RootScene组件后,创建BoxComponent,也应该是private :

    RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
    RootComponent = RootScene;
    CollisionBox =   CreateDefaultSubobject<UBoxComponent>(TEXT("Collision Box"));
  7. 使用SetupAttachment功能将其连接到RootComponent:

    CollisionBox->SetupAttachment(RootComponent);
  8. 将其BoxExtent属性设置为所有轴上的60单位。这将使BoxComponent的规模增加一倍(120 x 120 x 120) :

    CollisionBox->SetBoxExtent(FVector(60.0f, 60.0f, 60.0f));
  9. 使用SetRelativeLocation功能:

    CollisionBox->SetRelativeLocation(FVector(0.0f, 0.0f,   120.0f));

    将其在 Z 轴上的相对位置偏移120个单位

  10. Now, you will require a function that will listen to the BoxComponent's OnBeginOverlap event. This event will be called whenever an object enters the BoxComponent. This function must be preceded by the UFUNCTION macro, be public, return nothing, and have the following parameters:

```cpp
UFUNCTION()
void OnBeginOverlap(UPrimitiveComponent* OverlappedComp,   AActor* OtherActor, UPrimitiveComponent* OtherComp, int32   OtherBodyIndex, bool bFromSweep, const FHitResult&   SweepResult);
```

参数如下:

*   `UPrimitiveComponent* OverlappedComp`:重叠的属于这个演员的组件。
*   `AActor* OtherActor`:参与重叠的另一个演员。
*   `UPrimitiveComponent* OtherComp`:重叠的属于另一个行动者的成分。
*   `int32 OtherBodyIndex`:被命中的图元中的项目的索引(通常对实例化静态网格组件有用)。
*   `bool bFromSweep`:重叠是否源于扫掠痕迹。
*   `FHitResult& SweepResult`: The data of the Sweep Trace resulting from the collision between this object and the other object.

    注意

    虽然我们不会在这个项目中使用`OnEndOverlap`事件,但您很可能迟早会需要使用它,所以下面是该事件所需的函数签名,它看起来与我们刚刚了解到的非常相似:

    `UFUNCTION()`

    `void OnEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);`
  1. 接下来,我们需要将这个函数绑定到BoxComponentOnComponentBeginOverlap事件:
```cpp
CollisionBox->OnComponentBeginOverlap.AddDynamic(this,   &AVictoryBox::OnBeginOverlap);
```
  1. Within our OnBeginOverlap function implementation, we're going to check whether the actor we overlapped is a DodgeballCharacter. Because we'll be referencing this class, we also need to include it:
```cpp
#include "DodgeballCharacter.h" 
void AVictoryBox::OnBeginOverlap(UPrimitiveComponent *   OverlappedComp, AActor * OtherActor, UPrimitiveComponent *   OtherComp, int32 OtherBodyIndex, bool bFromSweep, const   FHitResult & SweepResult)
{
  if (Cast<ADodgeballCharacter>(OtherActor))
  {
  }
}
```

如果我们重叠的演员是一个`DodgeballCharacter`,我们要退出游戏。
  1. 我们将为此目的使用KismetSystemLibraryKismetSystemLibrary类包含在您的项目中通用的有用函数:
```cpp
#include "Kismet/KismetSystemLibrary.h"
```
  1. In order to quit the game, we will call KismetSystemLibrary's QuitGame function. This function receives the following:
```cpp
UKismetSystemLibrary::QuitGame(GetWorld(),
  nullptr,
  EQuitPreference::Quit,
  true);
```

前面代码片段中的重要参数解释如下:

*   一个`World`对象,我们可以用`GetWorld`功能访问。
*   一个`PlayerController`对象,我们将其设置为`nullptr`。我们这样做是因为这个函数会自动找到一个。
*   一个`EQuitPreference`对象,意思是我们想要结束游戏的方式,要么退出,要么只是把它作为一个后台进程。我们会想实际退出游戏,而不仅仅是把它作为一个后台进程。
*   A `bool`, which indicates whether we want to ignore the platform's restrictions when it comes to quitting the game, which we will set to `true`.

    接下来,我们将创建我们的蓝图类。
  1. 编译你的修改,打开编辑器,进入Content Browser里面的ContentThirdPersonCPPBlueprint目录,新建一个继承自VictoryBox的蓝图类,命名为BP_VictoryBox。打开该资产并进行以下修改: * 添加新的静态网格组件。 * 将其StaticMesh属性设置为Floor_400x400。 * 将其Material属性设置为M_Metal_Gold。 * 在所有三个轴上将其刻度设置为0.75单位。 * Set its location to (-150, -150, 20), on the X, Y, and Z axes, respectively.

    完成这些更改后,蓝图的“视口”选项卡应该如下所示:

    Figure 6.23: Victory box placed in the Blueprint's Viewport tab

图 6.23:放置在蓝图视口选项卡中的胜利框

将蓝图放入您的级别,测试它的功能:

Figure 6.24: Victory Box blueprint in the level for testing

图 6.24:测试级别的胜利箱蓝图

如果你玩了关卡,踩在金盘上(和碰撞框重叠),你会注意到游戏突然结束,正如预期的那样。

就这样,我们结束了VictoryBox课!现在,您知道如何在自己的项目中使用重叠事件了。您可以使用这些事件创建多种游戏机制,因此祝贺您完成本练习。

我们现在已经非常接近本章的结尾,在这里我们将完成一个新的活动,但是首先,我们需要对我们的DodgeballProjectile类进行一些修改,即在它的ProjectileMovementComponent中添加一个 getter 函数,这将在下一个练习中进行。

getter 函数是一个只返回特定属性而不做其他事情的函数。这些函数通常被标记为内联的,这意味着当代码编译时,对该函数的调用将简单地被其内容替换。它们通常也被标记为const,因为它们不修改类的任何属性。

练习 6.06:在闪避弹中添加项目移动组件获取器函数

在本练习中,我们将向DodgeballProjectile类的ProjectileMovement属性添加一个 getter 函数,以便其他类可以访问它并修改它的属性。我们将在本章的活动中做同样的事情。

为此,您需要遵循以下步骤:

  1. 在 Visual Studio 中打开DodgeballProjectile类的头文件。

  2. Add a new public function called GetProjectileMovementComponent. This function will be an inline function, which in UE4's version of C++ is replaced with the FORCEINLINE macro. The function should also return a UProjectileMovementComponent* and be a const function:

    FORCEINLINE class UProjectileMovementComponent*   GetProjectileMovementComponent() const
    {
      return ProjectileMovement;
    }

    注意

    对特定函数使用FORCEINLINE宏时,不能将该函数的声明添加到头文件中,也不能将其实现添加到源文件中。两者必须在头文件中同时完成,如前所示。

至此,我们结束这个快速练习。在这里,我们已经给我们的DodgeballProjectile类添加了一个简单的getter函数,我们将在本章的活动中使用这个函数,我们将用SpawnActorDeferred函数替换EnemyCharacter类中的SpawnActor函数。这将允许我们在生成一个实例之前安全地编辑我们的DodgeballProjectile类的属性。

活动 6.01:用 EnemyCharacter 中指定的 SpawnActor 替换 SpawnActor 函数

在本活动中,您将更改 EnemyCharacter 的ThrowDodgeball功能,以便使用SpawnActorDeferred功能而不是SpawnActor功能,这样我们就可以在生成DodgeballProjectile之前更改其InitialSpeed

以下步骤将帮助您完成本活动:

  1. 在 Visual Studio 中打开EnemyCharacter类的源文件。

  2. 转到ThrowDodgeball功能的实现。

  3. 因为SpawnActorDeferred函数不能只接收种子位置和旋转属性,而必须接收FTransform属性,所以我们需要在调用该函数之前创建一个这样的属性。让我们称之为SpawnTransform并按此顺序发送产卵轮换和位置,作为其构造器的输入,这将分别是这个敌人的轮换和SpawnLocation属性。

  4. 然后,将SpawnActor函数调用更新为SpawnActorDeferred函数调用。代替发送产卵位置和产卵旋转作为它的第二和第三个参数,用我们刚刚创建的SpawnTransform属性替换它们,作为第二个参数。

  5. Make sure you save the return value of this function call inside a ADodgeballProjectile* property called Projectile.

    完成此操作后,您将成功创建一个新的DodgeballProjectile对象。然而,我们仍然需要改变它的InitialSpeed属性,并实际上衍生它

  6. 调用SpawnActorDeferred函数后,调用Projectile属性的GetProjectileMovementComponent函数,该函数返回其射弹运动分量,并将其InitialSpeed属性更改为2200单位。

  7. 因为我们将在EnemyCharacter类中访问属于投射物运动组件的属性,所以我们需要包含该组件,就像我们在练习 6.02中所做的那样,向躲避球投射物添加投射物运动组件。

  8. 更改了InitialSpeed属性的值后,剩下要做的唯一事情就是调用Projectile属性的FinishSpawning函数,该函数将接收我们创建的SpawnTransform属性作为参数。

  9. After you've done this, compile your changes and open the editor.

    预期产出:

    Figure 6.25: Dodgeball thrown at the player

图 6.25:向玩家投掷躲避球

注意

这个活动的解决方案可以在:https://packt.live/338jEBx找到。

通过完成本活动,您已经巩固了SpawnActorDeferred功能的使用,并知道如何在未来的项目中使用它。

总结

在本章中,您学习了如何使用物理模拟影响对象,创建自己的对象类型和碰撞预设,使用OnHitOnBeginOverlapOnEndOverlap事件,更新对象的物理材质,以及使用计时器。

现在,您已经学习了碰撞主题的这些基本概念,您将能够在创建自己的项目时提出新的和创造性的方法来使用它们。

在下一章中,我们将了解 Actor 组件、接口和蓝图函数库,它们对于保持项目的复杂性可管理性和高度模块化非常有用,从而允许您轻松地将一个项目的部分内容添加到另一个项目中。