C++ UE4 脚本编程秘籍(二)

原文:zh.annas-archive.org/md5/244B225FA5E3FFE01C9887B1851E5B64

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:处理事件和委托

Unreal 使用事件有效地通知类有关游戏世界中发生的事情。事件和委托对于确保可以以最小化类耦合的方式发出这些通知以及允许任意类订阅以接收通知非常有用。

本章中将介绍以下教程:

  • 通过虚函数实现事件处理

  • 创建一个绑定到 UFUNCTION 的委托

  • 取消注册委托

  • 创建一个带有输入参数的委托

  • 使用委托绑定传递有效负载数据

  • 创建一个多播委托

  • 创建一个自定义事件

  • 创建一个时间处理程序

  • 为第一人称射击游戏创建一个重生拾取物

通过虚函数实现事件处理

Unreal 提供的一些ActorComponent类包括以虚函数形式的事件处理程序。本教程将向您展示如何通过覆盖相关的虚函数来自定义这些处理程序。

操作步骤…

  1. 在编辑器中创建一个空的Actor。将其命名为MyTriggerVolume

  2. 将以下代码添加到类头文件中:

UPROPERTY()
UBoxComponent* TriggerZone;

UFUNCTION()
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
UFUNCTION()
virtual void NotifyActorEndOverlap(AActor* OtherActor) override;
  1. 将前述函数的实现添加到 cpp 文件中:
void AMyTriggerVolume::NotifyActorBeginOverlap(AActor* OtherActor)
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("%s entered me"),*(OtherActor->GetName())));
}

void AMyTriggerVolume::NotifyActorEndOverlap(AActor* OtherActor)
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("%s left me"), *(OtherActor->GetName())));
}
  1. 编译您的项目,并将MyTriggerActor的一个实例放入级别中。通过走进体积并查看屏幕上打印的输出来验证重叠/触摸事件是否已处理:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工作原理…

  1. 与往常一样,我们首先声明一个UPROPERTY来保存对我们组件子对象的引用。然后创建两个UFUNCTION声明。这些标记为virtualoverride,以便编译器理解我们要替换父类的实现,并且我们的函数实现可以被替换。

  2. 在函数的实现中,我们使用FString::printf从预设文本创建一个FString,并替换一些数据参数。

  3. 请注意,FString OtherActor->GetName()返回,并在传递给FString::Format之前使用*运算符进行解引用。不这样做会导致错误。

  4. 然后将此FString传递给全局引擎函数AddOnScreenDebugMessage

  5. -1的第一个参数告诉引擎允许重复字符串,第二个参数是消息显示的持续时间(以秒为单位),第三个参数是颜色,第四个参数是要打印的实际字符串。

  6. 现在,当我们的 Actor 的组件与其他物体重叠时,其UpdateOverlaps函数将调用NotifyActorBeginOverlap,并且虚函数分发将调用我们的自定义实现。

创建一个绑定到 UFUNCTION 的委托

委托允许我们调用一个函数,而不知道分配了哪个函数。它们是原始函数指针的更安全版本。本教程向您展示如何将UFUNCTION与委托关联,以便在执行委托时调用它。

准备工作

确保您已按照之前的步骤创建了一个TriggerVolume类。

操作步骤…

  1. 在我们的GameMode头文件中,在类声明之前使用以下宏声明委托:
DECLARE_DELEGATE(FStandardDelegateSignature)
UCLASS()
class UE4COOKBOOK_API AUE4CookbookGameMode : public AGameMode
  1. 向我们的游戏模式添加一个新成员:
FStandardDelegateSignature MyStandardDelegate;
  1. 创建一个名为DelegateListener的新Actor类。将以下内容添加到该类的声明中:
UFUNCTION()
void EnableLight();

UPROPERTY()
UPointLightComponent* PointLight;
  1. 在类实现中,将以下内容添加到构造函数中:
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
PointLight->SetVisibility(false);
  1. DelegateListener.cpp文件中,在项目的include文件和DelegateListener头文件之间添加#include "UE4CookbookGameMode.h"。在DelegateListener::BeginPlay实现中,添加以下内容:
Super::BeginPlay();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  if (MyGameMode != nullptr)
  {
    MyGameMode->MyStandardDelegate.BindUObject(this, &ADelegateListener::EnableLight);
  }
}
  1. 最后,实现EnableLight
void ADelegateListener::EnableLight()
{
  PointLight->SetVisibility(true);
}
  1. 将以下代码放入我们的 TriggerVolume 的NotifyActorBeginOverlap函数中:
UWorld* TheWorld = GetWorld();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  MyGameMode->MyStandardDelegate.ExecuteIfBound();
}
  1. 确保在 CPP 文件中也添加#include "UE4CookbookGameMode.h",以便编译器在使用之前知道该类。

  2. 编译您的游戏。确保您的游戏模式设置在当前级别中(如果您不知道如何设置,请参阅第四章中的使用 SpawnActor 实例化 Actor教程,Actors and Components),并将TriggerVolume的副本拖到级别中。还将DelegateListener的副本拖到级别中,并将其放置在平面表面上方约 100 个单位处:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 当您点击播放,并走进 Trigger volume 覆盖的区域时,您应该看到我们添加到DelegateListenerPointLight组件打开:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工作原理…

  1. 在我们的GameMode头文件中,声明一个不带任何参数的委托类型,称为FTriggerHitSignature

  2. 然后,我们在GameMode类的成员中创建委托的实例。

  3. 我们在DelegateListener中添加一个PointLight组件,以便我们有一个委托被执行的可视表示。

  4. 在构造函数中,我们初始化我们的PointLight,然后禁用它。

  5. 我们重写BeginPlay。我们首先调用父类的BeginPlay()实现。然后,我们获取游戏世界,使用GetGameMode()检索GameMode类。

  6. 将生成的AGameMode*转换为我们的GameMode类的指针需要使用Cast模板函数。

  7. 然后,我们可以访问GameMode的委托实例成员,并将我们的EnableLight函数绑定到委托,这样当委托被执行时就会调用它。

  8. 在这种情况下,我们绑定到UFUNCTION(),所以我们使用BindUObject。如果我们想要绑定到一个普通的 C++类函数,我们将使用BindRaw。如果我们想要绑定到一个静态函数,我们将使用BindStatic()

  9. TriggerVolume与玩家重叠时,它检索GameMode,然后在委托上调用ExecuteIfBound

  10. ExecuteIfBound检查委托是否绑定了函数,然后为我们调用它。

  11. EnableLight函数在被委托对象调用时启用PointLight组件。

另请参阅

  • 接下来的部分,取消委托,向您展示了如何在Listener在委托被调用之前被销毁的情况下安全地取消注册委托绑定

取消委托

有时,有必要移除委托绑定。这就像将函数指针设置为nullptr,这样它就不再引用已被删除的对象。

准备工作

您需要按照先前的教程进行操作,以便您有一个要取消注册的委托。

操作步骤…

  1. DelegateListener中,添加以下重写函数声明:
UFUNCTION()
virtual void EndPlay(constEEndPlayReason::Type EndPlayReason) override;
  1. 实现如下功能:
void ADelegateListener::EndPlay(constEEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  UWorld* TheWorld = GetWorld();
  if (TheWorld != nullptr)
  {
    AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
    AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
    if (MyGameMode != nullptr)
    {
      MyGameMode->MyStandardDelegate.Unbind();
    }
  }
}

工作原理…

  1. 本教程将本章迄今为止的两个先前教程结合起来。我们重写EndPlay,这是一个作为虚函数实现的事件,这样我们就可以在DelegateListener离开游戏时执行代码。

  2. 在重写的实现中,我们在委托上调用Unbind()方法,这将从DelegateListener实例中取消链接成员函数。

  3. 如果不这样做,委托就会像指针一样悬空,当DelegateListener离开游戏时,它就处于无效状态。

创建接受输入参数的委托

到目前为止,我们使用的委托没有接受任何输入参数。本教程向您展示如何更改委托的签名,以便它接受一些输入。

准备工作

确保您已经按照本章开头的教程进行了操作,该教程向您展示了如何创建TriggerVolume和我们为本教程所需的其他基础设施。

操作步骤…

  1. GameMode添加一个新的委托声明:
DECLARE_DELEGATE_OneParam(FParamDelegateSignature, FLinearColor)
  1. GameMode添加新成员:
FParamDelegateSignatureMyParameterDelegate;
  1. 创建一个名为ParamDelegateListener的新Actor类。将以下内容添加到声明中:
UFUNCTION()
void SetLightColor(FLinearColorLightColor);
UPROPERTY()
UPointLightComponent* PointLight;
  1. 在类实现中,将以下内容添加到构造函数中:
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
  1. ParamDelegateListener.cpp文件中,在项目的include文件和ParamDelegateListener头文件之间添加#include "UE4CookbookGameMode.h"。在ParamDelegateListener::BeginPlay实现内部添加以下内容:
Super::BeginPlay();
UWorld* TheWorld = GetWorld();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  if (MyGameMode != nullptr)
  {
    MyGameMode->MyParameterDelegate.BindUObject(this, &AParamDelegateListener::SetLightColor);
  }
}
  1. 最后,实现SetLightColor
void AParamDelegateListener::SetLightColor(FLinearColorLightColor)
{
  PointLight->SetLightColor(LightColor);
}
  1. 在我们的TriggerVolume中,在NotifyActorBeginOverlap中,在调用MyStandardDelegate.ExecuteIfBound之后添加以下行:
MyGameMode->MyParameterDelegate.ExecuteIfBound(FLinearColor(1, 0, 0, 1));

它是如何工作的…

  1. 我们的新委托签名使用了一个稍微不同的宏来声明。请注意DECLARE_DELEGATE_OneParam末尾的_OneParam后缀。正如你所期望的,我们还需要指定参数的类型。

  2. 就像我们创建没有参数的委托时一样,我们需要在我们的GameMode类的成员中创建委托的实例。

  3. 我们现在创建了一个新类型的DelegateListener,它期望将参数传递到绑定到委托的函数中。

  4. 当我们为委托调用ExecuteIfBound()方法时,我们现在需要传入将插入函数参数的值。

  5. 在我们绑定的函数内部,我们使用参数来设置灯光的颜色。

  6. 这意味着TriggerVolume不需要知道任何关于ParamDelegateListener的信息,就可以调用它的函数。委托使我们能够最小化两个类之间的耦合。

另请参阅

  • 取消注册委托食谱向您展示了如何在监听器在调用委托之前被销毁时安全取消注册委托绑定

使用委托绑定传递有效负载数据

只需进行最小的更改,就可以在创建时将参数传递给委托。本食谱向您展示了如何指定要始终作为参数传递给委托调用的数据。这些数据在绑定创建时计算,并且从那时起不会改变。

准备工作

确保您已经按照之前的步骤进行操作。我们将扩展之前的步骤的功能,以将额外的创建时参数传递给我们绑定的委托函数。

如何做…

  1. 在您的AParamDelegateListener::BeginPlay函数内部,将对BindUObject的调用更改为以下内容:
MyGameMode->MyParameterDelegate.BindUObject(this, &AParamDelegateListener::SetLightColor, false);
  1. SetLightColor的声明更改为:
void SetLightColor(FLinearColorLightColor, bool EnableLight);
  1. 修改SetLightColor的实现如下:
void AParamDelegateListener::SetLightColor(FLinearColorLightColor, bool EnableLight)
{
  PointLight->SetLightColor(LightColor);
  PointLight->SetVisibility(EnableLight);
}
  1. 编译并运行您的项目。验证当您走进TriggerVolume时,灯光会关闭,因为在绑定函数时传入了错误的有效负载参数。

它是如何工作的…

  1. 当我们将函数绑定到委托时,我们指定了一些额外的数据(在本例中是一个值为false的布尔值)。您可以以这种方式传递多达四个“有效负载”变量。它们会应用于您的函数,而不是您使用的DECLARE_DELEGATE_*宏中声明的任何参数之后。

  2. 我们更改了委托的函数签名,以便它可以接受额外的参数。

  3. 在函数内部,我们使用额外的参数根据编译时的值是 true 还是 false 来打开或关闭灯光。

  4. 我们不需要更改对ExecuteIfBound的调用 - 委托系统会自动首先应用通过ExecuteIfBound传入的委托参数,然后应用任何有效负载参数,这些参数始终在对BindUObject的调用中函数引用之后指定。

另请参阅

  • 食谱取消注册委托向您展示了如何在监听器在调用委托之前被销毁时安全取消注册委托绑定

创建多播委托

本章迄今为止使用的标准委托本质上是一个函数指针 - 它们允许您在一个特定对象实例上调用一个特定函数。多播委托是一组函数指针,每个指针可能在不同的对象上,当委托被广播时,它们都将被调用。

准备工作

这个示例假设你已经按照本章的初始示例进行了操作,因为它向你展示了如何创建用于广播多播委托的TriggerVolume

如何做…

  1. GameMode头文件中添加新的委托声明:
DECLARE_MULTICAST_DELEGATE(FMulticastDelegateSignature)
  1. 创建一个名为MulticastDelegateListener的新的Actor类。将以下内容添加到声明中:
UFUNCTION()
void ToggleLight();
UFUNCTION()
virtual void EndPlay(constEEndPlayReason::Type EndPlayReason) override;

UPROPERTY()
UPointLightComponent* PointLight;

FDelegateHandleMyDelegateHandle;
  1. 在类实现中,将此添加到构造函数中:
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
  1. MulticastDelegateListener.cpp文件中,在您项目的include文件和MulticastDelegateListener头文件包含之间添加#include "UE4CookbookGameMode.h"。在MulticastDelegateListener::BeginPlay实现中,添加以下内容:
Super::BeginPlay();
UWorld* TheWorld = GetWorld();
if (TheWorld != nullptr)
{
  AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
  AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
  if (MyGameMode != nullptr)
  {
    MyDelegateHandle  = MyGameMode->MyMulticastDelegate.AddUObject(this, &AMulticastDelegateListener::ToggleLight);
  }
}
  1. 实现ToggleLight
void AMulticastDelegateListener::ToggleLight()
{
  PointLight->ToggleVisibility();
}
  1. 实现我们的EndPlay重写函数:
void AMulticastDelegateListener::EndPlay(constEEndPlayReason::Type EndPlayReason)
{
  Super::EndPlay(EndPlayReason);
  UWorld* TheWorld = GetWorld();
  if (TheWorld != nullptr)
  {
    AGameMode* GameMode = UGameplayStatics::GetGameMode(TheWorld);
    AUE4CookbookGameMode * MyGameMode = Cast<AUE4CookbookGameMode>(GameMode);
    if (MyGameMode != nullptr)
    {
      MyGameMode->MyMulticastDelegate.Remove(MyDelegateHandle);
    }
  }
}
  1. TriggerVolume::NotifyActorBeginOverlap()中添加以下行:
MyGameMode->MyMulticastDelegate.Broadcast();
  1. 编译并加载您的项目。将您的级别中的GameMode设置为我们的烹饪书游戏模式,然后将四到五个MulticastDelegateListener的实例拖到场景中。

  2. 步入TriggerVolume以查看所有MulticastDelegateListener切换其灯光的可见性。

工作原理…

  1. 正如你所期望的那样,委托类型需要明确声明为多播委托,而不是标准的单绑定委托。

  2. 我们的新Listener类与我们原始的DelegateListener非常相似。主要区别在于,我们需要在FDelegateHandle中存储对委托实例的引用。

  3. 当演员被销毁时,我们可以使用存储的FDelegateHandle作为Remove()的参数,安全地将自己从绑定到委托的函数列表中移除。

  4. Broadcast()函数是ExecuteIfBound()的多播等效。与标准委托不同,无需提前检查委托是否绑定,也不需要像ExecuteIfBound一样调用。无论绑定了多少个函数,甚至没有绑定任何函数,Broadcast()都是安全运行的。

  5. 当我们在场景中有多个多播监听器实例时,它们将分别向在GameMode中实现的多播委托注册自己。

  6. 然后,当TriggerVolume与玩家重叠时,它会广播委托,每个监听器都会收到通知,导致它们切换其关联点光的可见性。

  7. 多播委托可以以与标准委托完全相同的方式接受参数。

创建自定义事件

自定义委托非常有用,但它们的一个限制是它们可以被一些其他第三方类外部广播,也就是说,它们的 Execute/Broadcast 方法是公开可访问的。

有时,您可能需要一个委托,可以由其他类外部分配,但只能由包含它们的类广播。这是事件的主要目的。

准备工作

确保您已经按照本章的初始示例进行了操作,以便您拥有MyTriggerVolumeCookBookGameMode的实现。

如何做…

  1. 将以下事件声明宏添加到您的MyTriggerVolume类的头文件中:
DECLARE_EVENT(AMyTriggerVolume, FPlayerEntered)
  1. 向类添加已声明事件签名的实例:
FPlayerEnteredOnPlayerEntered;
  1. AMyTriggerVolume::NotifyActorBeginOverlap中添加此内容:
OnPlayerEntered.Broadcast();
  1. 创建一个名为TriggerVolEventListener的新的Actor类。

  2. 向其声明中添加以下类成员:

UPROPERTY()
UPointLightComponent* PointLight;

UPROPERTY(EditAnywhere)
AMyTriggerVolume* TriggerEventSource;
UFUNCTION()
void OnTriggerEvent();
  1. 在类构造函数中初始化PointLight
PointLight = CreateDefaultSubobject<UPointLightComponent>("PointLight");
RootComponent = PointLight;
  1. BeginPlay中添加以下内容:
if (TriggerEventSource != nullptr)
{
  TriggerEventSource->OnPlayerEntered.AddUObject(this, &ATriggerVolEventListener::OnTriggerEvent);
}
  1. 最后,实现OnTriggerEvent()
void ATriggerVolEventListener::OnTriggerEvent()
{
  PointLight->SetLightColor(FLinearColor(0, 1, 0, 1));
}
  1. 编译您的项目,并启动编辑器。创建一个级别,其中游戏模式设置为我们的UE4CookbookGameMode,然后将ATriggerVolEventListenerAMyTriggerVolume的一个实例拖到级别中。

  2. 选择TriggerVolEventListener,您将在详细信息面板中的类别中看到TriggerVolEventListener列出,其中包含属性Trigger Event Source外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 使用下拉菜单选择您的AMyTriggerVolume实例,以便监听器知道要绑定到哪个事件:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 玩游戏,并进入触发体积的影响区域。验证您的EventListener的颜色是否变为绿色。

它是如何工作的…

  1. 与所有其他类型的代表一样,事件需要它们自己的特殊宏函数。

  2. 第一个参数是事件将被实现到的类。这将是唯一能够调用Broadcast()的类,所以确保它是正确的类。

  3. 第二个参数是我们新事件函数签名的类型名称。

  4. 我们在我们的类中添加了这种类型的实例。虚幻文档建议使用On<x>作为命名惯例。

  5. 当某物与我们的TriggerVolume重叠时,我们调用我们自己事件实例的广播。

  6. 在新类中,我们创建一个点光源作为事件被触发的可视表示。

  7. 我们还创建了一个指向TriggerVolume的指针来监听事件。我们将UPROPERTY标记为EditAnywhere,因为这样可以在编辑器中设置它,而不必使用GetAllActorsOfClass或其他方式在程序中获取引用。

  8. 最后是我们的事件处理程序,当某物进入TriggerVolume时。

  9. 我们像往常一样在构造函数中创建和初始化我们的点光源。

  10. 游戏开始时,监听器检查我们的TriggerVolume引用是否有效,然后将我们的OnTriggerEvent函数绑定到TriggerVolume事件。

  11. OnTriggerEvent中,我们将灯光的颜色改为绿色。

  12. 当某物进入TriggerVolume时,它会导致TriggerVolume调用自己的事件广播。然后我们的TriggerVolEventListener就会调用其绑定的方法,改变我们灯光的颜色。

创建一个时间处理程序

这个教程向您展示了如何使用前面介绍的概念来创建一个演员,它通知其他演员游戏内时间的流逝。

如何做…

  1. 创建一个名为TimeOfDayHandler的新的Actor类。

  2. 在头文件中添加一个多播代表声明:

DECLARE_MULTICAST_DELEGATE_TwoParams(FOnTimeChangedSignature, int32, int32)
  1. 将我们的代表的一个实例添加到类声明中:
FOnTimeChangedSignatureOnTimeChanged;
  1. 将以下属性添加到类中:
UPROPERTY()
int32 TimeScale;

UPROPERTY()
int32 Hours;
UPROPERTY()
int32 Minutes;

UPROPERTY()
float ElapsedSeconds;
  1. 将这些属性的初始化添加到构造函数中:
TimeScale = 60;
Hours = 0;
Minutes = 0;
ElapsedSeconds = 0;
  1. Tick中,添加以下代码:
ElapsedSeconds += (DeltaTime * TimeScale);
if (ElapsedSeconds> 60)
{
  ElapsedSeconds -= 60;
  Minutes++;
  if (Minutes > 60)
  {
    Minutes -= 60;
    Hours++;
  }

  OnTimeChanged.Broadcast(Hours, Minutes);
}
  1. 创建一个名为Clock的新的Actor类。

  2. 将以下属性添加到类头部:

UPROPERTY()
USceneComponent* RootSceneComponent;

UPROPERTY()
UStaticMeshComponent* ClockFace;
UPROPERTY()
USceneComponent* HourHandle;
UPROPERTY()
UStaticMeshComponent* HourHand;
UPROPERTY()
USceneComponent* MinuteHandle;
UPROPERTY()
UStaticMeshComponent* MinuteHand;

UFUNCTION()
void TimeChanged(int32 Hours, int32 Minutes);
FDelegateHandleMyDelegateHandle;
  1. 在构造函数中初始化和转换组件:
RootSceneComponent = CreateDefaultSubobject<USceneComponent>("RootSceneComponent");
ClockFace = CreateDefaultSubobject<UStaticMeshComponent>("ClockFace");
HourHand = CreateDefaultSubobject<UStaticMeshComponent>("HourHand");
MinuteHand = CreateDefaultSubobject<UStaticMeshComponent>("MinuteHand");
HourHandle = CreateDefaultSubobject<USceneComponent>("HourHandle");
MinuteHandle = CreateDefaultSubobject<USceneComponent>("MinuteHandle");
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cylinder.Cylinder'"));
if (MeshAsset.Object != nullptr)
{
  ClockFace->SetStaticMesh(MeshAsset.Object);
  HourHand->SetStaticMesh(MeshAsset.Object);
  MinuteHand->SetStaticMesh(MeshAsset.Object);
}
RootComponent = RootSceneComponent;
HourHand->AttachTo(HourHandle);
MinuteHand->AttachTo(MinuteHandle);
HourHandle->AttachTo(RootSceneComponent);
MinuteHandle->AttachTo(RootSceneComponent);
ClockFace->AttachTo(RootSceneComponent);
ClockFace->SetRelativeTransform(FTransform(FRotator(90, 0, 0), FVector(10, 0, 0), FVector(2, 2, 0.1)));
HourHand->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(0, 0, 25), FVector(0.1, 0.1, 0.5)));
MinuteHand->SetRelativeTransform(FTransform(FRotator(0, 0, 0), FVector(0, 0, 50), FVector(0.1, 0.1, 1)));
  1. 将以下内容添加到BeginPlay中:
TArray<AActor*>TimeOfDayHandlers;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ATimeOfDayHandler::StaticClass(), TimeOfDayHandlers);
if (TimeOfDayHandlers.Num() != 0)
{
  auto TimeOfDayHandler = Cast<ATimeOfDayHandler>(TimeOfDayHandlers[0]);
  MyDelegateHandle = TimeOfDayHandler->OnTimeChanged.AddUObject(this, &AClock::TimeChanged);
}
  1. 最后,实现TimeChanged作为您的事件处理程序。
void AClock::TimeChanged(int32 Hours, int32 Minutes)
{
  HourHandle->SetRelativeRotation(FRotator( 0, 0,30 * Hours));
  MinuteHandle->SetRelativeRotation(FRotator(0,0,6 * Minutes));
}
  1. 在您的级别中放置一个TimeOfDayHandlerAClock的实例,并播放以查看时钟上的指针是否在旋转:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它是如何工作的…

  1. TimeOfDayHandler包含一个带有两个参数的代表,因此使用宏的TwoParams变体。

  2. 我们的类包含变量来存储小时、分钟和秒,以及TimeScale,这是一个用于加速测试目的的加速因子。

  3. 在处理程序的Tick函数中,我们根据自上一帧以来经过的时间累积经过的秒数。

  4. 我们检查经过的秒数是否超过了 60。如果是,我们减去 60,并增加Minutes

  5. 同样,对于Minutes——如果它们超过 60,我们减去 60,并增加Hours

  6. 如果MinutesHours被更新,我们会广播我们的代表,让订阅了代表的任何对象都知道时间已经改变。

  7. Clock actor 使用一系列场景组件和静态网格来构建类似时钟表盘的网格层次结构。

  8. Clock构造函数中,我们将层次结构中的组件进行父子关联,并设置它们的初始比例和旋转。

  9. BeginPlay中,时钟使用GetAllActorsOfClass()来获取级别中所有的time of day处理程序。

  10. 如果级别中至少有一个TimeOfDayHandlerClock就会访问第一个,并订阅其TimeChanged事件。

  11. TimeChanged事件触发时,时钟会根据当前时间的小时和分钟数旋转时针和分针。

为第一人称射击游戏创建一个重生拾取物

这个教程向您展示了如何创建一个可放置的拾取物,在一定时间后重新生成,适用于 FPS 中的弹药或其他拾取物。

如何做…

  1. 创建一个名为Pickup的新的Actor类。

  2. Pickup.h中声明以下委托类型:

DECLARE_DELEGATE(FPickedupEventSignature)
  1. 将以下属性添加到类头文件中:
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
UPROPERTY()
UStaticMeshComponent* MyMesh;

UPROPERTY()
URotatingMovementComponent* RotatingComponent;

FPickedupEventSignatureOnPickedUp;
  1. 将以下代码添加到构造函数中:
MyMesh = CreateDefaultSubobject<UStaticMeshComponent>("MyMesh");
RotatingComponent = CreateDefaultSubobject<URotatingMovementComponent>("RotatingComponent");
RootComponent = MyMesh;
auto MeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  MyMesh->SetStaticMesh(MeshAsset.Object);
}
MyMesh->SetCollisionProfileName(TEXT("OverlapAllDynamic"));
RotatingComponent->RotationRate = FRotator(10, 0, 10);
  1. 实现重写的NotifyActorBeginOverlap
void APickup::NotifyActorBeginOverlap(AActor* OtherActor)
{
  OnPickedUp.ExecuteIfBound();
}
  1. 创建第二个名为PickupSpawnerActor类。

  2. 将以下内容添加到类头文件中:

UPROPERTY()
USceneComponent* SpawnLocation;

UFUNCTION()
void PickupCollected();
UFUNCTION()
void SpawnPickup();
UPROPERTY()
APickup* CurrentPickup;
FTimerHandleMyTimer;
  1. PickupSpawner的实现文件中将Pickup.h添加到包含文件中。

  2. 在构造函数中初始化我们的根组件:

SpawnLocation = CreateDefaultSubobject<USceneComponent>("SpawnLocation");
  1. BeginPlay中使用SpawnPickup函数在游戏开始时生成一个拾取物:
SpawnPickup();
  1. 实现PickupCollected
void APickupSpawner::PickupCollected()
{
  GetWorld()->GetTimerManager().SetTimer(MyTimer, this, &APickupSpawner::SpawnPickup, 10, false);
  CurrentPickup->OnPickedUp.Unbind();
  CurrentPickup->Destroy();
}
  1. SpawnPickup创建以下代码:
void APickupSpawner::SpawnPickup()
{
  UWorld* MyWorld = GetWorld();
  if (MyWorld != nullptr){
    CurrentPickup = MyWorld->SpawnActor<APickup>(APickup::StaticClass(), GetTransform());
    CurrentPickup->OnPickedUp.BindUObject(this, &APickupSpawner::PickupCollected);
  }
}
  1. 编译并启动编辑器,然后将PickupSpawner的一个实例拖到关卡中。走到由旋转立方体表示的拾取物上,并验证它在 10 秒后再次生成:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工作原理…

  1. 像往常一样,在我们的Pickup内部创建一个委托,以便我们的 Spawner 可以订阅它,以便它知道玩家何时收集了拾取物。

  2. Pickup还包含一个静态网格作为视觉表示,以及一个RotatingMovementComponent,使网格以一种方式旋转,以吸引玩家的注意。

  3. Pickup构造函数中,我们加载引擎内置的网格作为我们的视觉表示。

  4. 我们指定网格将与其他对象重叠,然后在XZ轴上将网格的旋转速率设置为每秒 10 个单位。

  5. 当玩家与Pickup重叠时,它会从第一步触发其PickedUp委托。

  6. PickupSpawner有一个场景组件来指定生成拾取物的位置。它有一个执行此操作的函数,并且有一个UPROPERTY标记的对当前生成的Pickup的引用。

  7. PickupSpawner构造函数中,我们像往常一样初始化我们的组件。

  8. 游戏开始时,Spawner 运行其SpawnPickup函数。

  9. 这个函数生成我们的Pickup的一个实例,然后将APickupSpawner::PickupCollected绑定到新实例上的OnPickedUp函数。它还存储对当前实例的引用。

  10. 当玩家与Pickup重叠后,PickupCollected运行,创建一个定时器在 10 秒后重新生成拾取物。

  11. 移除到已收集拾取物的现有委托绑定,然后销毁拾取物。

  12. 10 秒后,定时器触发,再次运行SpawnActor,创建一个新的Pickup

第六章:输入和碰撞

本章涵盖了围绕游戏控制输入(键盘、鼠标和游戏手柄)以及与障碍物的碰撞相关的教程。

本章将涵盖以下教程:

  • 轴映射-键盘、鼠标和游戏手柄方向输入,用于 FPS 角色

  • 轴映射-标准化输入

  • 动作映射-用于 FPS 角色的单按钮响应

  • 从 C++添加轴和动作映射

  • 鼠标 UI 输入处理

  • UMG 键盘 UI 快捷键

  • 碰撞-使用忽略让物体相互穿过

  • 碰撞-使用重叠拾取物体

  • 碰撞-使用阻止防止相互穿透

介绍

良好的输入控件在您的游戏中非常重要。提供键盘、鼠标和尤其是游戏手柄输入将使您的游戏更受用户欢迎。

提示

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 您可以在 Windows PC 上使用 Xbox 360 和 PlayStation 控制器-它们具有 USB 输入。检查您当地的电子商店,以找到一些好的 USB 游戏控制器。您还可以使用无线控制器,连接到 PC 的游戏控制器无线接收器适配器。

轴映射-键盘、鼠标和游戏手柄方向输入,用于 FPS 角色

有两种类型的输入映射:轴映射动作映射。轴映射是您按住一段时间以获得其效果的输入(例如,按住W键移动玩家向前),而动作映射是一次性输入(例如,按下游戏手柄上的A键使玩家跳跃)。在本教程中,我们将介绍如何设置键盘、鼠标和游戏手柄轴映射输入控件以移动 FPS 角色。

准备就绪

您必须有一个 UE4 项目,其中有一个主角玩家,以及一个地面平面可供行走,以准备进行此操作。

如何做…

  1. 创建一个 C++类,Warrior,从Character派生:
UCLASS()
class CH6_API AWarrior : public ACharacter
{
  GENERATED_BODY()
};
  1. 启动 UE4,并根据您的Warrior类派生一个蓝图,BP_Warrior

  2. 创建并选择一个新的GameMode类蓝图,如下所示:

  3. 转到设置 | 项目设置 | 地图和模式

  4. 单击默认GameMode下拉菜单旁边的**+**图标,这将创建一个GameMode类的新蓝图,并选择您选择的名称(例如BP_GameMode)。

  5. 双击您创建的新BP_GameMode蓝图类以进行编辑。

  6. 打开您的BP_GameMode蓝图,并选择您的蓝图化的BP_Warrior类作为默认的Pawn类。

  7. 要设置键盘输入驱动玩家,打开设置 | 项目设置 | 输入。在接下来的步骤中,我们将完成在游戏中驱动玩家向前的过程:

  8. 单击轴映射标题旁边的**+**图标。

提示

轴映射支持连续(按住按钮)输入,而动作映射支持一次性事件。

  1. 为轴映射命名。第一个示例将展示如何移动玩家向前,因此将其命名为Forward

  2. Forward下方,选择一个键盘键来分配给此轴映射,例如W

  3. 单击Forward旁边的**+**图标,并选择一个游戏控制器输入,以将玩家前进映射到移动玩家的游戏控制器左拇指杆上。

  4. 使用键盘、游戏手柄和可选的鼠标输入绑定,完成轴映射的后退、左转和右转。

  5. 从您的 C++代码中,重写AWarrior类的SetupPlayerInputComponent函数,如下所示:

void AWarrior::SetupPlayerInputComponent(UInputComponent* Input)
{
  check(Input);
  Input->BindAxis( "Forward", this, &AWarrior::Forward );
}
  1. 在您的AWarrior类中提供一个Forward函数,如下所示:
void AWarrior::Forward( float amount )
{
  if( Controller && amount )
  {
    // Moves the player forward by an amount in forward direction
    AddMovementInput(GetActorForwardVector(), amount );
  }
}
  1. 编写并完成其余输入方向的函数,AWarrior::BackAWarrior::LeftAWarrior::Right

它是如何工作的…

UE4 引擎允许直接将输入事件连接到 C++函数调用。由输入事件调用的函数是某个类的成员函数。在前面的示例中,我们将W键的按下和手柄的左摇杆向上按下都路由到了AWarrior::Forward的 C++函数。调用AWarrior::Forward的实例是路由控制器输入的实例。这由在GameMode类中设置为玩家角色的对象控制。

另请参阅

  • 您可以实际上从 C++中编写Forward输入轴绑定,而不是在 UE4 编辑器中输入。我们将在以后的示例中详细描述这一点,从 C++添加轴和动作映射

轴映射 - 规范化输入

如果您注意到,右侧和前方的输入为 1.0 实际上会总和为 2.0 的速度。这意味着在对角线上移动可能比纯粹向前、向后、向左或向右移动更快。我们真正应该做的是夹住任何导致速度超过 1.0 单位的输入值,同时保持指示的输入方向。我们可以通过存储先前的输入值并覆盖::Tick()函数来实现这一点。

准备工作

打开一个项目,并设置一个Character派生类(我们称之为Warrior)。

如何做…

  1. 如下覆盖AWarrior::SetupPlayerInputComponent( UInputComponent* Input )函数:
void AWarrior::SetupPlayerInputComponent( UInputComponent* Input )
{
  Input->BindAxis( "Forward", this, &AWarrior::Forward );
  Input->BindAxis( "Back", this, &AWarrior::Back );
  Input->BindAxis( "Right", this, &AWarrior::Right );
  Input->BindAxis( "Left", this, &AWarrior::Left );
}
  1. 编写相应的::Forward::Back::Right::Left函数如下:
void AWarrior::Forward( float amount ) {
  // We use a += of the amount added so that
  // when the other function modifying .Y
  // (::Back()) affects lastInput, it won't
  // overwrite with 0's
  lastInput.Y += amount;
}
void AWarrior::Back( float amount ) {
  lastInput.Y += -amount;
}
void AWarrior::Right( float amount ) {
  lastInput.X += amount;
}
void AWarrior::Left( float amount ) {
  lastInput.X += -amount;
}
  1. AWarrior::Tick()函数中,在规范化输入向量中任何超大值后修改输入值:
void AWarrior::Tick( float DeltaTime ) {
  Super::Tick( DeltaTime );
  if( Controller )
  {
    float len = lastInput.Size();
    if( len > 1.f )
      lastInput /= len;
    AddMovementInput(
    GetActorForwardVector(), lastInput.Y );
    AddMovementInput(GetActorRightVector(), lastInput.X);
    // Zero off last input values
    lastInput = FVector2D( 0.f, 0.f );
  }
}

工作原理…

当输入向量超过 1.0 的幅度时,我们对其进行规范化。这将限制最大输入速度为 1.0 单位(例如,当完全向上和向右按下时,速度为 2.0 单位)。

动作映射 - 用于 FPS 角色的单按钮响应

动作映射用于处理单按钮按下(而不是按住的按钮)。对于应该按住的按钮,请确保使用轴映射。

准备工作

准备好一个带有您需要完成的操作的 UE4 项目,例如JumpShootGun

如何做…

  1. 打开设置 | 项目设置 | 输入

  2. 转到动作映射标题,并单击旁边的**+**图标。

  3. 开始输入应映射到按钮按下的操作。例如,为第一个动作输入Jump

  4. 选择要按下的键以执行该操作,例如,空格键

  5. 如果您希望通过另一个按键触发相同的操作,请单击动作映射名称旁边的**+**,然后选择另一个按键来触发该操作。

  6. 如果要求ShiftCtrlAltCmd键必须按下才能触发操作,请确保在键选择框右侧的复选框中指示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. 要将您的操作链接到 C++代码函数,您需要覆盖SetupPlayerInputComponent(UInputControl* control )函数。在该函数内输入以下代码:
voidAWarrior::SetupPlayerInputComponent(UInputComponent* Input)
{
  check(Input );
  // Connect the Jump action to the C++ Jump function
  Input->BindAction("Jump", IE_Pressed, this, &AWarrior::Jump );
}

工作原理…

动作映射是单按钮按下事件,触发 C++代码以响应它们运行。您可以在 UE4 编辑器中定义任意数量的操作,但请确保将动作映射与 C++中的实际按键绑定起来。

另请参阅

  • 您可以列出您希望从 C++代码映射的操作。有关此信息,请参阅从 C++添加轴和动作映射中的以下示例。

从 C++添加轴和动作映射

轴映射动作映射可以通过 UE4 编辑器添加到游戏中,但我们也可以直接从 C++代码中添加它们。由于 C++函数的连接本来就是从 C++代码进行的,因此您可能会发现在 C++中定义您的轴和动作映射也很方便。

准备工作

您需要一个 UE4 项目,您想要在其中添加一些轴和动作映射。如果您通过 C++代码添加它们,您可以删除Settings | Project Settings | Input中列出的现有轴和动作映射。要添加您的自定义轴和动作映射,有两个 C++函数您需要了解:UPlayerInput::AddAxisMappingUPlayerInput::AddActionMapping。这些是UPlayerInput对象上可用的成员函数。UPlayerInput对象位于PlayerController对象内,可以通过以下代码访问:

GetWorld()->GetFirstPlayerController()->PlayerInput

您还可以使用UPlayerInput的两个静态成员函数来创建您的轴和动作映射,如果您不想单独访问玩家控制器的话:

UPlayerInput::AddEngineDefinedAxisMapping()
UPlayerInput::AddEngineDefinedActionMapping()

如何做…

  1. 首先,我们需要定义我们的FInputAxisKeyMappingFInputActionKeyMapping对象,具体取决于您是连接轴键映射(用于按下按钮进行输入)还是连接动作键映射(用于一次性事件-按下按钮进行输入)。

  2. 对于轴键映射,我们定义一个FInputAxisKeyMapping对象,如下所示:

FInputAxisKeyMapping backKey( "Back", EKeys::S, 1.f );
  1. 这将包括动作的字符串名称,要按的键(使用 EKeys enum),以及是否应按住ShiftCtrlAltcmd(Mac)来触发事件。

  2. 对于动作键映射,定义FInputActionKeyMapping,如下所示:

FInputActionKeyMapping jump("Jump", EKeys::SpaceBar, 0, 0, 0, 0);
  1. 这将包括动作的字符串名称,要按的键,以及是否应按住ShiftCtrlAltcmd(Mac)来触发事件。

  2. 在您的玩家Pawn类的SetupPlayerInputComponent函数中,将您的轴和动作键映射注册到以下内容:

  3. 与特定控制器连接的PlayerInput对象:

GetWorld()->GetFirstPlayerController()->PlayerInput->AddAxisMapping( backKey ); // specific to a controller
  1. 或者,您可以直接注册到UPlayerInput对象的静态成员函数:
UPlayerInput::AddEngineDefinedActionMapping(jump );

提示

确保您对轴与动作映射使用了正确的函数!

  1. 使用 C++代码注册您的动作和轴映射到 C++函数,就像前两个示例中所示的那样:
Input->BindAxis("Back", this, &AWarrior::Back);
Input->BindAction("Jump", IE_Pressed, this, &AWarrior::Jump );

它是如何工作的…

动作和轴映射注册函数允许您直接从 C++代码设置您的输入映射。C++编码的输入映射本质上与在Settings | Project Settings | Input对话框中输入映射相同。

鼠标 UI 输入处理

在使用**虚幻运动图形(UMG)**工具包时,您会发现鼠标事件非常容易处理。我们可以注册 C++函数以在鼠标单击或与 UMG 组件的其他类型交互后运行。

通常,事件注册将通过蓝图进行;但在这个示例中,我们将概述如何编写和连接 UMG 事件的 C++函数。

准备工作

在您的 UE4 项目中创建一个 UMG 画布。从那里,我们将为OnClickedOnPressedOnReleased事件注册事件处理程序。

如何做…

  1. Content Browser中右键单击(或单击Add New),然后选择User Interface | Widget Blueprint,如下截图所示。这将向您的项目添加一个可编辑的小部件蓝图。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 双击您的Widget Blueprint进行编辑。

  3. 通过从左侧的调色板拖动按钮来向界面添加按钮。

  4. 滚动Details面板,直到找到Events子部分。

  5. 单击您想要处理的任何事件旁边的**+**图标。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  6. 将出现在蓝图中的事件连接到任何具有BlueprintCallable标签的 C++ UFUNCTION()。例如,在您的GameMode类派生中,您可以包括一个函数,如下:

UFUNCTION(BlueprintCallable, Category = UIFuncs)
void ButtonClicked()
{
  UE_LOG(LogTemp, Warning, TEXT( "UI Button Clicked" ) );
}
  1. 通过在您选择的事件下的蓝图图表中路由到它来触发函数调用。

  2. 通过在GameModeBegin Play函数中调用Create Widget,然后调用Add to Viewport来构建和显示您的 UI(或任何主要对象)。

它是如何工作的…

您的小部件蓝图的按钮事件可以轻松连接到蓝图事件,或通过前面的方法连接到 C++函数。

UMG 键盘 UI 快捷键

每个用户界面都需要与之关联的快捷键。要将这些程序到您的 UMG 界面中,您可以简单地将某些键组合连接到一个动作映射中。当动作触发时,只需调用与 UI 按钮本身触发相同的蓝图函数。

准备工作

您应该已经创建了一个 UMG 界面,就像前面的示例中所示的那样。

如何做…

  1. 设置 | 项目设置 | 输入中,为您的热键事件定义一个新的动作映射,例如HotKey_UIButton_Spell

  2. 将事件连接到您的 UI 的函数调用,无论是在蓝图中还是在 C++代码中。

工作原理…

通过将动作映射与 UI 调用的函数进行短路连接,可以使您在游戏程序中很好地实现热键。

碰撞 - 使用忽略让物体相互穿过

碰撞设置相当容易获得。碰撞有三类交集:

  • 忽略:相互穿过而没有任何通知的碰撞。

  • 重叠:触发OnBeginOverlapOnEndOverlap事件的碰撞。允许具有重叠设置的对象相互渗透。

  • 阻止:阻止所有相互渗透的碰撞,并完全阻止物体相互重叠。

对象被归类为许多对象类型之一。特定蓝图组件的碰撞设置允许您将对象归类为您选择的对象类型,并指定该对象如何与所有其他类型的所有其他对象发生碰撞。这在蓝图编辑器的详细信息 | 碰撞部分以表格格式呈现。

例如,以下屏幕截图显示了角色的CapsuleComponent碰撞设置:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

准备工作

您应该有一个 UE4 项目,其中包含一些您希望为其编程交集的对象。

如何做…

  1. 打开蓝图编辑器,选择您希望其他对象只是穿过并忽略的对象。在组件列表下,选择您想要设置程序的组件。

  2. 选择您的组件后,查看您的详细信息标签(通常在右侧)。在碰撞预设下,选择无碰撞或**自定义…**预设。

  3. 如果选择无碰撞预设,您可以只需保持不变,所有碰撞都将被忽略。

  4. 如果选择**自定义…**预设,则选择以下之一:

  5. 无碰撞启用碰撞下拉菜单中。

  6. 启用碰撞下选择一个碰撞模式,确保为每个您希望忽略碰撞的对象类型勾选忽略复选框。

工作原理…

忽略的碰撞不会触发任何事件,也不会阻止标记为忽略的对象之间的相互渗透。

碰撞 - 使用重叠拾取物品

物品拾取是一件非常重要的事情。在这个示例中,我们将概述如何使用 Actor 组件基元上的重叠事件使物品拾取起作用。

准备工作

前面的示例,碰撞:使用忽略让物体相互穿过,描述了碰撞的基础知识。在开始本示例之前,您应该阅读它以了解背景。我们将在这里创建一个**新对象通道…**来识别Item类对象,以便可以对其进行重叠的编程,只与玩家角色的碰撞体发生重叠。

如何做…

  1. 首先为Item对象的碰撞基元创建一个独特的碰撞通道。在项目设置 | 碰撞下,通过转到**新对象通道…**来创建一个新的对象通道!如何做…

  2. 将新的对象通道命名为Item

  3. 取你的Item角色并选择用于与玩家角色交叉拾取的基本组件。将该基本组件的对象类型设置为Item类的对象类型

  4. 勾选Pawn对象类型旁边的Overlap复选框,如下截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  5. 确保勾选Generate Overlap Events复选框。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  6. 选择将拾取物品的玩家角色,并选择他身上用于寻找物品的组件。通常,这将是他的CapsuleComponent。检查与Item对象的Overlap外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  7. 现在玩家重叠了物品,物品也重叠了玩家角色。我们必须双向信号重叠(Item重叠PawnPawn重叠Item)才能正常工作。确保Pawn交叉组件的Generate Overlap Events也被勾选。

  8. 接下来,我们必须完成OnComponentBeginOverlap事件,要么是对物品,要么是对玩家的拾取体积,使用蓝图或 C++代码。

  9. 如果你更喜欢蓝图,在 Coin 的可交叉组件的Details面板的Events部分,点击On Component Begin Overlap事件旁边的**+**图标。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  10. 使用出现在你的Actor蓝图图表中的OnComponentBeginOverlap事件,将蓝图代码连接到玩家的胶囊体积发生重叠时运行。

  11. 如果你更喜欢 C++,你可以编写并附加一个 C++函数到CapsuleComponent。在你的玩家角色类中编写一个成员函数,签名如下:

UFUNCTION(BlueprintNativeEvent, Category = Collision)
void OnOverlapsBegin( UPrimitiveComponent* Comp, AActor* OtherActor,
UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult );

提示

在 UE 4.13 中,OnOverlapsBegin 函数的签名已更改为:

OnOverlapsBegin( UPrimitiveComponent* Comp, AActor* OtherActor,UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitREsult& SweepResult );
  1. 在你的.cpp文件中完成OnOverlapsBegin()函数的实现,确保以_Implementation结束函数名:
void AWarrior::OnOverlapsBegin_Implementation( AActor*
OtherActor, UPrimitiveComponent* OtherComp,
int32 OtherBodyIndex,
bool bFromSweep, const FHitResult& SweepResult )
{
  UE_LOG(LogTemp, Warning, TEXT( "Overlaps began" ) );
}
  1. 然后,提供一个PostInitializeComponents()覆盖,将OnOverlapsBegin()函数与你的角色类中的胶囊体重叠连接起来,如下所示:
void AWarrior::PostInitializeComponents()
{
  Super::PostInitializeComponents();
  if(RootComponent )
  {
    // Attach contact function to all bounding components.
    GetCapsuleComponent()->OnComponentBeginOverlap.AddDynamic( this, &AWarrior::OnOverlapsBegin );
    GetCapsuleComponent()->OnComponentEndOverlap.AddDynamic( this, &AWarrior::OnOverlapsEnd );
  }
}

它是如何工作的…

引擎引发的Overlap事件允许代码在两个 UE4Actor组件重叠时运行,而不会阻止对象的相互穿透。

碰撞 - 使用阻挡来防止穿透

阻挡意味着在引擎中将阻止Actor组件相互穿透,并且在发现碰撞后,任何两个基本形状之间的碰撞将被解决,不会重叠。

准备工作

从一个具有附加到它们的碰撞基元的对象的 UE4 项目开始(SphereComponentsCapsuleComponentsBoxComponents)。

如何做…

  1. 打开你想要阻挡另一个角色的角色的蓝图。例如,我们希望玩家角色阻挡其他玩家角色实例。

  2. Details面板中标记你不希望与其他组件相互穿透的角色内的基元,将这些组件标记为Blocking外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它是如何工作的…

当对象相互阻挡时,它们将不被允许相互穿透。任何穿透将被自动解决,并且对象将被推开。

还有更多…

你可以重写OnComponentHit函数,以便在两个对象相撞时运行代码。这与OnComponentBeginOverlap事件是不同的。

第七章:类和接口之间的通信

本章向您展示如何编写自己的 UInterfaces,并演示如何在 C++中利用它们来最小化类耦合并帮助保持代码清晰。本章将涵盖以下内容:

  • 创建一个UInterface

  • 在对象上实现UInterface

  • 检查类是否实现了UInterface

  • 在本地代码中实现UInterface的转换

  • 从 C++调用本地UInterface函数

  • 相互继承UInterface

  • 在 C++中重写UInterface函数

  • 从本地基类向蓝图公开UInterface方法

  • 在蓝图中实现UInterface函数

  • 创建 C++ UInterface函数实现,可以在蓝图中重写

  • 从 C++调用蓝图定义的接口函数

  • 使用 UInterfaces 实现简单的交互系统

介绍

在您的游戏项目中,有时需要一系列潜在不同的对象共享共同的功能,但使用继承是不合适的,因为这些不同对象之间没有“是一个”关系。诸如 C++的语言倾向于使用多重继承来解决这个问题。

然而,在虚幻中,如果您希望从父类中的函数都可以在蓝图中访问,您需要将它们都设置为UCLASS。这有两个问题。在同一个对象中两次继承UClass会破坏UObject应该形成一个整洁的可遍历层次结构的概念。这也意味着对象上有两个UClass方法的实例,并且它们在代码中必须明确区分。虚幻代码库通过从 C#借用一个概念来解决这个问题——显式接口类型。

使用这种方法的原因是,与组合相比,组件只能在 Actor 上使用,而不能在一般的 UObjects 上使用。接口可以应用于任何UObject。此外,这意味着我们不再对对象和组件之间的“是一个”关系进行建模;相反,它只能表示“有一个”关系。

创建一个 UInterface

UInterfaces 是一对类,它们一起工作,使类能够在多个类层次结构中表现多态行为。本章向您展示了纯粹使用代码创建UInterface的基本步骤。

如何做…

  1. UInterfaces 不会出现在虚幻中的主类向导中,因此我们需要使用 Visual Studio 手动添加类。

  2. 解决方案资源管理器中右键单击文件夹,然后选择添加 | 新建项

  3. 选择一个.h文件开始,命名为MyInterface.h

  4. 确保将项目中项目的目录更改为 Intermediate 到 Source/ProjectName。

  5. 单击OK在项目文件夹中创建一个新的头文件。

  6. 重复步骤,以创建MyInterface.cpp作为您的实现文件。

  7. 将以下代码添加到头文件中:

#include "MyInterface.generated.h"
/**  */
UINTERFACE()
class UE4COOKBOOK_API UMyInterface: public UInterface
{
  GENERATED_BODY()
};

/**  */
class UE4COOKBOOK_API IMyInterface
{
  GENERATED_BODY()

  public:
  virtualFStringGetTestName();
};
  1. .cpp文件中使用以下代码实现类:
#include "UE4Cookbook.h"
#include "MyInterface.h"

FString IMyInterface::GetTestName()
{
  unimplemented();
  return FString();
}
  1. 编译您的项目以验证代码是否没有错误地编写。

它是如何工作的…

  1. UInterfaces 被实现为接口头文件中声明的一对类。

  2. 与往常一样,因为我们正在利用虚幻的反射系统,我们需要包含我们生成的头文件。有关更多信息,请参阅第五章中关于通过虚拟函数实现的事件处理,处理事件和委托。

  3. 与继承自UObject的类一样,它使用UCLASS,我们需要使用UINTERFACE宏来声明我们的新UInterface

  4. 该类被标记为UE4COOKBOOK_API,以帮助导出库符号。

  5. UObject部分的接口的基类是UInterface

  6. 就像UCLASS类型一样,我们需要在类的主体中放置一个宏,以便自动生成的代码被插入其中。

  7. 对于 UInterfaces,该宏是GENERATED_BODY()。该宏必须放在类主体的开头。

  8. 第二个类也被标记为UE4COOKBOOK_API,并且以特定的方式命名。

  9. 请注意,UInterface派生类和标准类具有相同的名称,但具有不同的前缀。UInterface派生类具有前缀U,标准类具有前缀I

  10. 这很重要,因为这是 Unreal Header Tool 期望类的命名方式,以使其生成的代码正常工作。

  11. 普通的本机接口类需要其自动生成的内容,我们使用GENERATED_BODY()宏包含它。

  12. 我们在IInterface内声明了类应该在内部实现的函数。

  13. 在实现文件中,我们实现了我们的UInterface的构造函数,因为它是由 Unreal Header Tool 声明的,并且需要一个实现。

  14. 我们还为我们的GetTestName()函数创建了一个默认实现。如果没有这个,编译的链接阶段将失败。这个默认实现使用unimplemented()宏,当代码行被执行时会发出调试断言。

另请参阅

  • 参考第五章中的使用委托绑定传递有效负载数据处理事件和委托;特别是第一个示例解释了我们在这里应用的一些原则

在对象上实现 UInterface

确保您已经按照前面的示例准备好要实现的UInterface

操作步骤…

  1. 使用 Unreal Wizard 创建一个名为SingleInterfaceActor的新的Actor类。

  2. IInterface—在本例中为IMyInterface—添加到我们新的Actor类的公共继承列表中:

class UE4COOKBOOK_API ASingleInterfaceActor : public AActor, public IMyInterface
  1. 为我们希望重写的IInterface函数在类中添加一个override声明:
FStringGetTestName() override;
  1. 通过添加以下代码在实现文件中实现重写的函数:
FStringASingleInterfaceActor::GetTestName()
{
  return IMyInterface::GetTestName();
}

工作原理…

  1. C++使用多重继承来实现接口,因此我们在这里利用了这种机制,声明了我们的SingleInterfaceActor类,其中添加了public IMyInterface

  2. 我们从IInterface而不是UInterface继承,以防止SingleInterfaceActor继承两个UObject的副本。

  3. 鉴于接口声明了一个virtual函数,如果我们希望自己实现它,我们需要使用 override 修饰符重新声明该函数。

  4. 在我们的实现文件中,我们实现了我们重写的virtual函数。

  5. 在我们的函数重写中,为了演示目的,我们调用函数的基本IInterface实现。或者,我们可以编写自己的实现,并完全避免调用基类的实现。

  6. 我们使用IInterface:: specifier而不是Super,因为Super指的是我们类的父类UClass,而 IInterfaces 不是 UClasses(因此没有U前缀)。

  7. 您可以根据需要在对象上实现第二个或多个 IInterfaces。

检查类是否实现了 UInterface

按照前两个示例,以便您有一个我们可以检查的UInterface,以及实现接口的类,可以对其进行测试。

操作步骤…

  1. 在您的游戏模式实现中,将以下代码添加到BeginPlay函数中:
FTransformSpawnLocation;
ASingleInterfaceActor* SpawnedActor = GetWorld()->SpawnActor<ASingleInterfaceActor> (ASingleInterfaceActor::StaticClass(), SpawnLocation);
if (SpawnedActor->GetClass()->ImplementsInterface(UMyInterface::StaticClass()))
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, TEXT("Spawned actor implements interface!"));
}
  1. 鉴于我们引用了ASingleInterfaceActorIMyInterface,我们需要在我们的源文件中#include MyInterface.hSingleInterfaceActor.h

工作原理…

  1. BeginPlay中,我们创建一个空的FTransform函数,它的默认值是所有平移和旋转分量的0,因此我们不需要显式设置任何分量。

  2. 然后,我们使用UWorld中的SpawnActor函数,这样我们就可以创建我们的SingleActorInterface的实例,并将指针存储到临时变量中。

  3. 然后,我们使用GetClass()在我们的实例上获取一个引用到其关联的UClass。我们需要一个对UClass的引用,因为该对象是保存对象的所有反射数据的对象。

  4. 反射数据包括对象上所有UPROPERTY的名称和类型,对象的继承层次结构,以及它实现的所有接口的列表。

  5. 因此,我们可以在UClass上调用ImplementsInterface(),如果对象实现了所讨论的UInterface,它将返回true

  6. 如果对象实现了接口,因此从ImplementsInterface返回true,我们就会在屏幕上打印一条消息。

另请参阅

  • 第五章, 处理事件和委托,有许多与生成 actor 相关的配方

在本机代码中实现 UInterface 的转换

作为开发人员,UInterfaces 为您提供的一个优势是,使用Cast< >来处理转换,可以将实现共同接口的异构对象集合视为相同对象的集合。

注意

请注意,如果您的类通过 Blueprint 实现接口,则此方法将无效。

准备工作

您应该为此配方准备一个UInterface和一个实现接口的Actor

使用 Unreal 中的向导创建一个新的游戏模式,或者可选地,重用以前配方中的项目和GameMode

操作步骤…

  1. 打开游戏模式的声明,并向其中添加一个新的UPROPERTY()宏
UPROPERTY()
TArray<IMyInterface*>MyInterfaceInstances;
  1. 在头文件的包含部分添加#include "MyInterface.h"

  2. 在游戏模式的BeginPlay实现中添加以下内容:

for (TActorIterator<AActor> It(GetWorld(), AActor::StaticClass()); It; ++It)
{
  AActor* Actor = *It;
  IMyInterface* MyInterfaceInstance = Cast<IMyInterface>(Actor);
  if (MyInterfaceInstance)
  {
    MyInterfaceInstances.Add(MyInterfaceInstance);
  }
}
GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, FString::Printf(TEXT("%d actors implement the interface"), MyInterfaceInstances.Num()));
  1. 将级别的游戏模式覆盖设置为您的游戏模式,然后将几个实现自定义接口的 actor 实例拖放到级别中。

  2. 当您播放级别时,屏幕上应该打印一条消息,指示在级别中实现了接口的实例的数量:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它是如何工作的…

  1. 我们创建了一个指向MyInterface实现的指针数组。

  2. BeginPlay中,我们使用TActorIterator<AActor>来获取我们级别中的所有Actor实例。

  3. TActorIterator有以下构造函数:

explicitTActorIterator( UWorld* InWorld, TSubclassOf<ActorType>InClass = ActorType::StaticClass() )
: Super(InWorld, InClass )
  1. TActorIterator期望一个要操作的世界,以及一个UClass实例来指定我们感兴趣的 Actor 类型。

  2. ActorIterator是类似 STL 迭代器类型的迭代器。这意味着我们可以编写以下形式的for循环:

for (iterator-constructor;iterator;++iterator)
  1. 在循环内,我们取消引用迭代器以获取Actor指针。

  2. 然后,我们尝试将其转换为我们的接口;如果它实现了它,这将返回一个指向接口的指针,否则将返回nullptr

  3. 因此,我们可以检查接口指针是否为null,如果不是,我们可以将接口指针引用添加到我们的数组中。

  4. 最后,一旦我们遍历了TActorIterator中的所有 actor,我们就可以在屏幕上显示一条消息,显示实现了接口的项目的计数。

从 C++调用本机 UInterface 函数

按照前一个配方来理解将Actor指针转换为接口指针。

注意

请注意,由于此配方依赖于前一个配方中使用的转换技术,因此它只能与使用 C++实现接口的对象一起使用,而不能与 Blueprint 一起使用。这是因为 Blueprint 类在编译时不可用,因此在技术上不继承该接口。

操作步骤…

  1. 使用编辑向导创建一个新的Actor类。将其命名为AntiGravityVolume

  2. BoxComponent添加到新的Actor中。

UPROPERTY()
UBoxComponent* CollisionComponent;
  1. 在头文件中重写以下Actor virtual函数:
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
virtual void NotifyActorEndOverlap(AActor* OtherActor) override;
  1. 在源文件中创建一个实现,如下所示:
voidAAntiGravityVolume::NotifyActorBeginOverlap(AActor* OtherActor)
{
  IGravityObject* GravityObject = Cast<IGravityObject>(OtherActor);
  if (GravityObject != nullptr)
  {
    GravityObject->DisableGravity();
  }
}

voidAAntiGravityVolume::NotifyActorEndOverlap(AActor* OtherActor)
{
  IGravityObject* GravityObject = Cast<IGravityObject>(OtherActor);
  if (GravityObject != nullptr)
  {
    GravityObject->EnableGravity();
  }
}
  1. 在构造函数中初始化BoxComponent
AAntiGravityVolume::AAntiGravityVolume()
{
  PrimaryActorTick.bCanEverTick = true;
  CollisionComponent = CreateDefaultSubobject<UBoxComponent>("CollisionComponent");
  CollisionComponent->SetBoxExtent(FVector(200, 200, 400));
  RootComponent = CollisionComponent;

}
  1. 创建一个名为GravityObject的接口。

  2. IGravityObject中添加以下virtual函数:

virtual void EnableGravity();
virtual void DisableGravity();
  1. IGravityObject实现文件中创建virtual函数的默认实现:
voidIGravityObject::EnableGravity()
{
  AActor* ThisAsActor = Cast<AActor>(this);
  if (ThisAsActor != nullptr)
  {
    TArray<UPrimitiveComponent*>PrimitiveComponents;
    ThisAsActor->GetComponents(PrimitiveComponents);
    for (UPrimitiveComponent* Component : PrimitiveComponents)
    {
      Component->SetEnableGravity(true);
    }
  }
}

voidIGravityObject::DisableGravity()
{
  AActor* ThisAsActor = Cast<AActor>(this);
  if (ThisAsActor != nullptr)
  {
    TArray<UPrimitiveComponent*>PrimitiveComponents;
    ThisAsActor->GetComponents(PrimitiveComponents);
    for (UPrimitiveComponent* Component : PrimitiveComponents)
    {
      Component->SetEnableGravity(false);
    }
  }
}
  1. 创建一个名为PhysicsCubeActor子类。

  2. 添加一个静态网格:

UPROPERTY()
UStaticMeshComponent* MyMesh;
  1. 在构造函数中初始化组件:
MyMesh = CreateDefaultSubobject<UStaticMeshComponent>("MyMesh");
autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  MyMesh->SetStaticMesh(MeshAsset.Object);
}
MyMesh->SetMobility(EComponentMobility::Movable);
MyMesh->SetSimulatePhysics(true);
SetActorEnableCollision(true);
  1. 要使PhysicsCube实现GravityObject,首先在头文件中#include "GravityObject.h",然后修改类声明:
class UE4COOKBOOK_API APhysicsCube : public AActor, public IGravityObject
  1. 编译您的项目。

  2. 创建一个新的关卡,并在场景中放置一个重力体积的实例。

  3. 在重力体积上放置一个PhysicsCube的实例,然后稍微旋转它,使其有一个角落比其他角落低,如下图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 验证当对象进入体积时重力被关闭,然后再次打开。

注意

请注意,重力体积不需要知道任何关于您的PhysicsCube actor 的信息,只需要知道重力对象接口。

工作原理…

  1. 我们创建一个新的Actor类,并添加一个箱子组件,以便给角色添加一个会与角色发生碰撞的物体。或者,如果您想要使用 BSP 功能来定义体积的形状,您也可以对AVolume进行子类化。

  2. 重写NotifyActorBeginOverlapNotifyActorEndOverlap,以便在对象进入或离开AntiGravityVolume区域时执行某些操作。

  3. NotifyActorBeginOverlap实现中,我们尝试将与我们发生重叠的对象转换为IGravityObject指针。

  4. 这个测试是为了检查所讨论的对象是否实现了该接口。

  5. 如果指针有效,则对象确实实现了接口,因此可以安全地使用接口指针调用对象上的接口方法。

  6. 鉴于我们在NotifyActorBeginOverlap内部,我们希望禁用对象上的重力,因此我们调用DisableGravity()

  7. NotifyActorEndOverlap内部,我们执行相同的检查,但是我们重新启用了对象的重力。

  8. DisableGravity的默认实现中,我们将我们自己的指针(this指针)转换为AActor

  9. 这使我们能够确认接口仅在Actor子类上实现,并调用在AActor中定义的方法。

  10. 如果指针有效,我们知道我们是一个Actor,所以我们可以使用GetComponents<class ComponentType>()来从自身获取特定类型的所有组件的TArray

  11. GetComponents是一个template函数。它需要一些模板参数:

template<class T, class AllocatorType>
voidGetComponents(TArray<T*, AllocatorType>&OutComponents) const
  1. 自 2014 年标准以来,C++支持模板参数的编译时推断。这意味着如果编译器可以从我们提供的普通函数参数中推断出模板参数,那么在调用函数时我们不需要实际指定模板参数。

  2. TArray的默认实现是template<typename T, typename Allocator = FDefaultAllocator>TArray;

  3. 这意味着我们不需要默认情况下指定分配器,因此当我们声明数组时,我们只使用TArray<UPrimitiveComponent*>

  4. TArray传递到GetComponents函数中时,编译器知道它实际上是TArray<UPrimitiveComponent*, FDefaultAllocator>,并且能够填充模板参数TAllocatorType,所以在函数调用时不需要这两个作为模板参数。

  5. GetComponents遍历Actor拥有的组件,并且从typename T继承的任何组件都有指针存储在PrimitiveComponents数组中。

  6. 使用基于范围的for循环,这是 C++的另一个新特性,我们可以在不需要使用传统的for循环结构的情况下迭代函数放入我们的TArray中的组件。

  7. 对每个组件调用SetEnableGravity(false),这将禁用重力。

  8. 同样,EnableGravity函数遍历了 actor 中包含的所有 primitive 组件,并使用SetEnableGravity(true)启用了重力。

另请参阅

  • 查看第四章, Actors and Components, 详细讨论了演员和组件。第五章, 处理事件和委托, 讨论了诸如NotifyActorOverlap之类的事件。

相互继承 UInterface

有时,您可能需要创建一个更通用的UInterface专门用于UInterface

这个配方向您展示了如何使用 UInterfaces 继承来专门化一个Killable接口,使其具有无法通过正常手段杀死的Undead接口。

操作步骤…

  1. 创建一个名为UKillableUINTERFACE/IInterface

  2. UInterface声明中添加UINTERFACE(meta=(CannotImplementInterfaceInBlueprint))

  3. 在头文件中添加以下函数:

UFUNCTION(BlueprintCallable, Category=Killable)
virtual bool IsDead();
UFUNCTION(BlueprintCallable, Category = Killable)
virtual void Die();
  1. 在实现文件中为接口提供默认实现:
boolIKillable::IsDead()
{
  return false;
}

voidIKillable::Die()
{
  GEngine->AddOnScreenDebugMessage(-1,1, FColor::Red,"Arrrgh");
  AActor* Me = Cast<AActor>(this);
  if (Me)
  {
    Me->Destroy();
  }

}
  1. 创建一个新的UINTERFACE/IInterface称为Undead。修改它们继承自UKillable/IKillable
UINTERFACE()
class UE4COOKBOOK_API UUndead: public UKillable
{
  GENERATED_BODY()
};

/**  */
class UE4COOKBOOK_API IUndead: public IKillable
{
  GENERATED_BODY()

};
  1. 确保您包含了定义Killable接口的头文件。

  2. 在新接口中添加一些重写和新的方法声明:

virtual bool IsDead() override;
virtual void Die() override;
virtual void Turn();
virtual void Banish();
  1. 为函数创建实现:
boolIUndead::IsDead()
{
  return true;
}

voidIUndead::Die()
{
  GEngine->AddOnScreenDebugMessage(-1,1, FColor::Red,"You can't kill what is already dead. Mwahaha");
}

voidIUndead::Turn()
{
  GEngine->AddOnScreenDebugMessage(-1,1, FColor::Red, "I'm fleeing!");

}

voidIUndead::Banish()
{
  AActor* Me = Cast<AActor>(this);
  if (Me)
  {
    Me->Destroy();
  }
}
  1. 在 C++中创建两个新的Actor类:一个名为Snail,另一个名为Zombie

  2. Snail类设置为实现IKillable接口,并添加适当的头文件#include

  3. 同样,将Zombie类设置为实现IUndead,并#include "Undead.h"

  4. 编译您的项目。

  5. 启动编辑器,将ZombieSnail的实例拖入你的关卡中。

  6. 关卡蓝图中为它们添加引用。

  7. 在每个引用上调用Die(消息)。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  8. 连接两个消息调用的执行引脚,然后将其连接到Event BeginPlay

运行游戏,然后验证Zombie对您的杀死尝试不屑一顾,但Snail呻吟着然后死去(从世界大纲中移除)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工作原理…

  1. 为了能够在关卡蓝图中测试这个配方,我们需要使接口函数可以通过蓝图调用,所以我们需要在我们的UFUNCTION上加上BlueprintCallable修饰符。

  2. 然而,在UInterface中,编译器默认期望接口可以通过 C++和蓝图实现。这与BlueprintCallable冲突,后者仅表示该函数可以从蓝图中调用,而不是可以在其中被重写。

  3. 我们可以通过将接口标记为CannotImplementInterfaceInBlueprint来解决冲突。

  4. 这使得我们可以使用BlueprintCallable作为我们的UFUNCTION修饰符,而不是BlueprintImplementableEvent(由于额外的代码允许通过蓝图重写函数而产生额外的开销)。

  5. 我们将IsDeadDie定义为virtual,以使它们可以在另一个继承此类的 C++类中被重写。

  6. 在我们的默认接口实现中,IsDead总是返回false

Die的默认实现在屏幕上打印死亡消息,然后销毁实现此接口的对象(如果它是一个Actor)。

  1. 现在我们可以创建一个名为Undead的第二个接口,它继承自Killable

  2. 我们在类声明中使用public UKillable/public IKillable来表示这一点。

  3. 当然,结果是我们需要包含定义Killable接口的头文件。

  4. 我们的新接口重写了Killable定义的两个函数,以提供更合适的UndeadIsDead/Die定义。

  5. 我们的重写定义已经通过从IsDead返回true来使Undead已经死亡。

  6. DieUndead上调用时,我们只是打印一条消息,Undead嘲笑我们试图再次杀死它的微弱尝试,并且什么也不做。

  7. 我们还可以为我们的Undead特定函数指定默认实现,即Turn()Banish()

  8. Undead被转化时,它们会逃跑,为了演示目的,我们在屏幕上打印一条消息。

  9. 然而,如果Undead被放逐,它们将被消灭并毁灭得无影无踪。

  10. 为了测试我们的实现,我们创建了两个Actors,每个都继承自两个接口中的一个。

  11. 在我们的级别中添加每个角色的一个实例后,我们使用级别蓝图来访问级别的BeginPlay事件。

  12. 当关卡开始播放时,我们使用消息调用来尝试在我们的实例上调用Die函数。

  13. 打印出来的消息是不同的,并且对应于两个函数实现,显示了 Zombie 对Die的实现是不同的,并且已经覆盖了 Snail 的实现。

在 C++中重写 UInterface 函数

UInterfaces 允许 C++中的继承的一个副作用是,我们可以在子类以及蓝图中覆盖默认实现。这个操作步骤向你展示了如何做到这一点。

准备工作

按照从 C++调用本机 UInterface 函数的步骤创建一个 Physics Cube,以便你已经准备好这个类。

操作步骤…

  1. 创建一个名为Selectable的新接口。

  2. ISelectable中定义以下函数:

virtual bool IsSelectable();

virtual bool TrySelect();

virtual void Deselect();
  1. 为这样的函数提供默认实现:
boolISelectable::IsSelectable()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Selectable");
  return true;
}

boolISelectable::TrySelect()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Accepting Selection");
  return true;
}

voidISelectable::Deselect()
{
  unimplemented();
}
  1. 创建一个基于APhysicsCube的类,名为SelectableCube

  2. SelectableCube类的头文件中包含#include "Selectable.h"

  3. 修改ASelectableCube的声明如下:

class UE4COOKBOOK_API ASelectableCube : public APhysicsCube, public ISelectable
  1. 将以下函数添加到头文件中:
ASelectableCube();
virtual void NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVectorHitLocation, FVectorHitNormal, FVectorNormalImpulse, constFHitResult& Hit) override;
  1. 实现以下函数:
ASelectableCube::ASelectableCube()
: Super()
{
  MyMesh->SetNotifyRigidBodyCollision(true);
}

voidASelectableCube::NotifyHit(class UPrimitiveComponent* MyComp, AActor* Other, class UPrimitiveComponent* OtherComp, bool bSelfMoved, FVectorHitLocation, FVectorHitNormal, FVectorNormalImpulse, constFHitResult& Hit)
{
  if (IsSelectable())
  {
    TrySelect();
  }
}
  1. 创建一个名为NonSelectableCube的新类,它继承自SelectableCube

  2. NonSelectableCube应该覆盖SelectableInterface中的函数:

virtual bool IsSelectable() override;

virtual bool TrySelect() override;

virtual void Deselect() override;
  1. 实现文件应该被修改以包括以下内容:
boolANonSelectableCube::IsSelectable()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Not Selectable");
  return false;
}

boolANonSelectableCube::TrySelect()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "Refusing Selection");
  return false;
}

voidANonSelectableCube::Deselect()
{
  unimplemented();
}
  1. SelectableCube的实例放置在离地面一定范围的级别中,并播放游戏。当方块触地时,您应该收到验证该角色可选择并已接受选择的消息。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 删除SelectableCube并用NonSelectableCube的实例替换,以查看替代消息,指示该角色不可选择,并拒绝选择。

它是如何工作的…

  1. 我们在Selectable接口中创建了三个函数。

  2. IsSelectable返回一个布尔值,表示对象是否可选择。你可以避免这样做,只需使用TrySelect,因为它返回一个布尔值来表示成功,但是,例如,你可能想知道你的 UI 内的对象是否是有效的选择,而不必实际尝试。

  3. TrySelect实际上尝试选择对象。没有明确的合同强制用户在尝试选择对象时尊重IsSelectable,因此TrySelect的命名是为了传达选择可能并不总是成功。

  4. 最后,Deselect是一个添加的函数,允许对象处理失去玩家选择。这可能涉及更改 UI 元素,停止声音或其他视觉效果,或者只是从单位周围移除选择轮廓。

  5. 函数的默认实现返回true表示IsSelectable(默认情况下,任何对象都是可选择的),返回true表示TrySelect(选择尝试总是成功),如果在没有被类实现的情况下调用Deselect,则会发出调试断言。

  6. 如果愿意,也可以将Deselect实现为纯虚函数。

  7. SelectableCube是一个新的类,继承自PhysicsCube,同时实现了ISelectable接口。

  8. 它还覆盖了NotifyHit,这是在AActor中定义的一个virtual函数,当演员经历RigidBody碰撞时触发。

  9. 我们在SelectableCube的实现中使用Super()构造函数调用来调用PhysicsCube的构造函数。然后,我们添加我们自己的实现,它在我们的静态网格实例上调用SetNotifyRigidBodyCollision(true)。这是必要的,因为默认情况下,刚体(例如具有碰撞的PrimitiveComponents)不会触发Hit事件,以进行性能优化。因此,我们重写的NotifyHit函数将永远不会被调用。

  10. NotifyHit的实现中,我们在自身上调用了一些ISelectable接口函数。鉴于我们知道我们是从ISelectable继承的对象,我们无需转换为ISelectable*即可调用它们。

  11. 我们使用IsSelectable来检查对象是否可选择,如果是,则尝试使用TrySelect来实际执行选择。

  12. NonSelectableCube继承自SelectableCube,因此我们可以强制该对象永远不可选择。

  13. 我们通过再次重写ISelectable接口函数来实现这一点。

  14. ANonSelectableCube::IsSelectable()中,我们在屏幕上打印一条消息,以便我们可以验证该函数是否被调用,然后返回false以指示该对象根本不可选择。

  15. 如果用户不尊重IsSelectable()ANonSelectableCube::TrySelect()始终返回false,以指示选择不成功。

  16. 鉴于不可能选择NonSelectableCubeDeselect()调用unimplemented(),这会引发一个断言警告,指出该函数未被实现。

  17. 现在,在播放场景时,每当SelectableCube/NonSelectableCube撞击另一个物体,导致刚体碰撞时,相关的角色将尝试选择自己,并在屏幕上打印消息。

另请参阅

  • 参见第六章,输入和碰撞,其中向您展示了如何从鼠标光标向游戏世界进行射线投射,并且可以用于扩展此示例以允许玩家点击物品进行选择

从本地基类向蓝图公开 UInterface 方法

能够在 C++中定义UInterface方法非常好,但它们也应该从蓝图中可访问。否则,使用蓝图的设计师或其他人将无法与您的UInterface进行交互。本示例向您展示了如何使接口中的函数在蓝图系统中可调用。

如何做…

  1. 创建一个名为UPostBeginPlay/IPostBeginPlayUInterface

  2. IPostBeginPlay添加以下virtual方法:

UFUNCTION(BlueprintCallable, Category=Test)
virtual void OnPostBeginPlay();
  1. 提供函数的实现:
voidIPostBeginPlay::OnPostBeginPlay()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, "PostBeginPlay called");
}
  1. 创建一个名为APostBeginPlayTest的新的Actor类。

  2. 修改类声明,使其还继承IPostBeginPlay

UCLASS()
class UE4COOKBOOK_API APostBeginPlayTest : public AActor, public IPostBeginPlay
  1. 编译您的项目。在编辑器内,将APostBeginPlayTest的实例拖入您的级别中。选择该实例,单击打开级别蓝图外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 在级别蓝图内,右键单击并创建对 PostBeginPlayTest1 的引用外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 从 actor 引用的右侧蓝色引脚拖动,然后在上下文菜单中搜索onpost,以查看您的新接口函数是否可用。单击它以在蓝图中插入对本机UInterface实现的调用。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 最后,将BeginPlay节点的执行引脚(白色箭头)连接到OnPostBeginPlay的执行引脚。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  5. 当您播放级别时,您应该看到屏幕上出现PostBeginPlay called的消息,验证蓝图已成功访问并调用了您的UInterface的本地代码实现。

它是如何工作的…

  1. UINTERFACE/IInterface对在其他示例中的功能一样,UInterface包含反射信息和其他数据,而IInterface作为实际的接口类,可以被继承。

  2. 允许IInterface内部函数暴露给蓝图的最重要的元素是UFUNCTION修饰符。

  3. BlueprintCallable标记此函数可以从蓝图系统中调用。

  4. 以任何方式暴露给蓝图的函数也需要一个Category值。这个Category值指定了函数在上下文菜单中将被列在哪个标题下。

  5. 该函数还必须标记为virtual,这样通过本地代码实现接口的类可以重写其中的函数实现。如果没有virtual修饰符,虚幻头部工具将给出一个错误,指示您必须添加virtualBlueprintImplementableEvent作为UFUNCTION修饰符。

  6. 这样做的原因是,如果没有这两者中的任何一个,接口函数将无法在 C++中被重写(由于缺少virtual),或者在蓝图中(因为缺少BlueprintImplementableEvent)。一个不能被重写,只能被继承的接口具有有限的实用性,因此 Epic 选择不在 UInterfaces 中支持它。

  7. 然后,我们提供了OnPostBeginPlay函数的默认实现,它使用GEngine指针来显示一个调试消息,确认函数被调用。

另请参阅

  • 有关如何将 C++类与蓝图集成的多个示例,请参阅第八章集成 C++和虚幻编辑器

在蓝图中实现 UInterface 函数

虚幻中 UInterface 的一个关键优势是用户能够在编辑器中实现UInterface函数。这意味着接口可以严格在蓝图中实现,而不需要任何 C++代码,这对设计师来说是有帮助的。

如何操作…

  1. 创建一个名为AttackAvoider的新UInterface

  2. 将以下函数声明添加到头文件:

UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = AttackAvoider)
voidAttackIncoming(AActor* AttackActor);
  1. 在编辑器中创建一个新的蓝图类外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 将类基于Actor外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 打开类设置外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 单击实现接口的下拉菜单,并选择AttackAvoider外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  5. 编译您的蓝图:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  6. 在事件图中右键单击,输入event attack。在上下文敏感菜单中,您应该看到Event Attack Incoming。选择它以在图表中放置一个事件节点:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  7. 从新节点的执行引脚中拖出,并释放。在上下文敏感菜单中输入print string以添加一个Print String节点。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  8. 您现在已经在蓝图中实现了一个UInterface函数。

工作原理…

  1. UINTERFACE/IInterface的创建方式与本章其他示例中看到的完全相同。

  2. 然而,当我们向接口添加一个函数时,我们使用一个新的UFUNCTION修饰符:BlueprintImplementableEvent

  3. BlueprintImplementableEvent 告诉虚幻头部工具生成代码,创建一个空的存根函数,可以由蓝图实现。我们不需要为函数提供默认的 C++实现。

  4. 我们在蓝图中实现接口,这样就可以以一种允许我们在蓝图中定义其实现的方式暴露函数。

  5. 头部工具生成的自动生成代码将UInterface函数的调用转发到我们的蓝图实现。

另请参阅

  • 以下示例向您展示了如何在 C++中为您的UInterface函数定义默认实现,然后在必要时在蓝图中进行覆盖

创建 C++ UInterface 函数实现,可以在蓝图中被覆盖

与以前的示例一样,UInterfaces 很有用,但如果设计者无法使用其功能,那么其效用将受到严重限制。

上一个示例向您展示了如何从蓝图中调用 C++ UInterface函数;这个示例将向您展示如何用自己的自定义蓝图函数替换UInterface函数的实现。

操作步骤…

  1. 创建一个名为WearableIWearableUWearable)的新接口。

  2. 在头文件中添加以下函数:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Wearable)
int32GetStrengthRequirement();
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Wearable)
boolCanEquip(APawn* Wearer);
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Wearable)
voidOnEquip(APawn* Wearer);
  1. 在实现文件中添加以下函数实现:
int32 IWearable::GetStrengthRequirement_Implementation()
{
  return 0;
}

Bool IWearable::CanEquip_Implementation(APawn* Wearer)
{
  return true;
}

Void IWearable::OnEquip_Implementation(APawn* Wearer)
{

}
  1. 在编辑器中创建一个名为Boots的新Actor类。

  2. Boots的头文件中添加#include "Wearable.h"

  3. 修改类声明如下:

UCLASS()
class UE4COOKBOOK_API ABoots : public AActor, public IWearable
  1. 添加我们接口创建的纯virtual函数的以下实现:
virtual void OnEquip_Implementation(APawn* Wearer) override
{
  IWearable::OnEquip_Implementation(Wearer);
}
virtual bool CanEquip_Implementation(APawn* Wearer) override
{
  return IWearable::CanEquip_Implementation(Wearer);
}
virtual int32 GetStrengthRequirement_Implementation() override
{
  return IWearable::GetStrengthRequirement_Implementation();
}
  1. 创建一个基于Actor的名为Gloves的新蓝图类。

  2. 在类设置中,选择Wearable作为Gloves角色将实现的接口。

  3. Gloves中,像这样重写OnEquip函数:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. GlovesBoots的副本拖到您的级别中进行测试。

  5. 在您的级别中添加以下蓝图代码:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  6. 验证Boots执行默认行为,但Gloves执行蓝图定义的行为。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

工作原理…

  1. 这个示例同时使用了两个UFUNCTION修饰符:BlueprintNativeEventBlueprintCallable

  2. BlueprintCallable在以前的示例中已经展示过,它是一种将UFUNCTION标记为在蓝图编辑器中可见和可调用的方法。

  3. BlueprintNativeEvent表示一个具有默认 C++(本机代码)实现的UFUNCTION,但也可以在蓝图中被覆盖。它是虚函数和BlueprintImplementableEvent的组合。

  4. 为了使这种机制工作,虚幻头部工具生成函数的主体,以便如果存在函数的蓝图版本,则调用该函数的蓝图版本;否则,将方法调用分派到本机实现。

  5. 为了将默认实现与分发功能分开,UHT 定义了一个新函数,该函数以您声明的函数命名,但在末尾添加了_Implementation

  6. 这就是为什么头文件声明了GetStrengthRequirement,但没有实现,因为那是自动生成的。

  7. 这也是为什么您的实现文件定义了GetStrengthRequirement_Implementation,但没有声明它,因为它也是自动生成的。

  8. Boots类实现了IWearable,但没有覆盖默认功能。但是,因为_Implementation函数被定义为virtual,我们仍然需要显式实现接口函数,然后直接调用默认实现。

  9. 相比之下,Gloves也实现了IWearable,但在蓝图中为OnEquip定义了一个重写的实现。

  10. 当我们使用级别蓝图调用这两个角色的OnEquip时,可以验证这一点。

从 C++调用蓝图定义的接口函数

虽然以前的示例侧重于 C++在蓝图中的可用性,比如能够从蓝图中调用 C++函数,并用蓝图覆盖 C++函数,但这个示例展示了相反的情况:从 C++调用蓝图定义的接口函数。

操作步骤…

  1. 创建一个名为UTalker/ITalker的新UInterface

  2. 添加以下UFUNCTION实现:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Talk)
void StartTalking();
  1. .cpp文件中提供一个默认的空实现:
void ITalker::StartTalking_Implementation()
{

}
  1. 创建一个基于StaticMeshActor的新类。

  2. 添加#include并修改类声明以包括 talker 接口:

#include "Talker.h"
class UE4COOKBOOK_API ATalkingMesh : public AStaticMeshActor, public ITalker
  1. 还要在类声明中添加以下函数:
void StartTalking_Implementation();
  1. 在实现中,将以下内容添加到构造函数中:
ATalkingMesh::ATalkingMesh()
:Super()
{
  autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
  if (MeshAsset.Object != nullptr)
  {
    GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
    //GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
    GetStaticMeshComponent()->bGenerateOverlapEvents = true;
  }
  GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
  SetActorEnableCollision(true);
}
Implmement the default implementation of our StartTalking function:
voidATalkingMesh::StartTalking_Implementation()
{
  GEngine->AddOnScreenDebugMessage(-1, 1, FColor::Red, TEXT("Hello there. What is your name?"));
}
  1. 创建一个基于DefaultPawn的新类,作为我们的玩家角色的功能。

  2. 在我们的类头文件中添加一些UPROPERTY/UFUNCTION

UPROPERTY()
UBoxComponent* TalkCollider;
UFUNCTION()
voidOnTalkOverlap(AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, constFHitResult&SweepResult);
  1. 修改构造函数:
ATalkingPawn::ATalkingPawn()
:Super()
{
  // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
  PrimaryActorTick.bCanEverTick = true;
  TalkCollider = CreateDefaultSubobject<UBoxComponent>("TalkCollider"); 
  TalkCollider->SetBoxExtent(FVector(200, 200, 100));
  TalkCollider->OnComponentBeginOverlap.AddDynamic(this, &ATalkingPawn::OnTalkOverlap);
  TalkCollider->AttachTo(RootComponent);
}
  1. 实现OnTalkOverlap
voidATalkingPawn::OnTalkOverlap(AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, constFHitResult&SweepResult)
{
  if (OtherActor->GetClass()->ImplementsInterface(UTalker::StaticClass()))
  {
    ITalker::Execute_StartTalking(OtherActor);
  }
}
  1. 创建一个新的GameMode,并将TalkingPawn设置为玩家的默认 pawn 类。

  2. 将您的ATalkingMesh类的一个实例拖入级别中。

  3. 通过右键单击它并从上下文菜单中选择适当的选项,基于ATalkingMesh创建一个新的蓝图类:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 将其命名为MyTalkingMesh

  5. 在蓝图编辑器中,创建一个像这样的StartTalking实现:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  6. 将您的新蓝图的副本拖入级别中,放在您的ATalkingMesh实例旁边。

  7. 走近这两个演员,并验证您的自定义 Pawn 是否正确调用了默认的 C++实现或蓝图实现。外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它是如何工作的…

  1. 一如既往,我们创建一个新的接口,然后在IInterface类中添加一些函数定义。

  2. 我们使用BlueprintNativeEvent说明符来指示我们希望在 C++中声明一个默认实现,然后可以在蓝图中进行重写。

  3. 我们创建了一个新的类(从StaticMeshActor继承以方便起见),并在其上实现了接口。

  4. 在新类构造函数的实现中,我们加载了一个静态网格,并像往常一样设置了我们的碰撞。

  5. 然后我们为我们的接口函数添加了一个实现,它只是在屏幕上打印一条消息。

  6. 如果您在一个完整的项目中使用这个,您可以播放动画,播放音频,修改用户界面,以及其他必要的操作来开始与您的Talker对话。

  7. 然而,此时,我们实际上没有任何东西来调用我们的Talker上的StartTalking

  8. 实现这一点的最简单方法是创建一个新的Pawn子类(再次从DefaultPawn继承以方便起见),它可以开始与任何与之发生碰撞的Talker演员交谈。

  9. 为了使其工作,我们创建了一个新的BoxComponent来建立我们将触发对话的半径。

  10. 一如既往,这是一个UPROPERTY,因此它不会被垃圾回收。

  11. 我们还为一个函数创建了定义,当新的BoxComponent与场景中的另一个Actor重叠时将被触发。

  12. 我们的TalkingPawn的构造函数初始化了新的BoxComponent,并适当设置了其范围。

  13. 构造函数还将OnTalkOverlap函数绑定为事件处理程序,以处理与我们的BoxComponent发生碰撞。

  14. 它还将盒组件附加到我们的RootComponent,以便随着玩家在级别中移动而移动。

  15. OnTalkOverlap内部,我们需要检查另一个演员是否实现了与我们的盒子重叠的Talker接口。

  16. 最可靠的方法是使用UClass中的ImplementsInterface函数。这个函数使用 Unreal Header Tool 在编译期间生成的类信息,并正确处理 C++和蓝图实现的接口。

  17. 如果函数返回true,我们可以使用我们的IInterface中包含的特殊自动生成的函数来调用我们实例上所选择的接口方法。

  18. 这是一个形式为<IInterface>::Execute_<FunctionName>的静态方法。在我们的实例中,我们的IInterfaceITalker,函数是StartTalking,所以我们要调用的函数是ITalker::Execute_StartTalking()

  19. 我们需要这个函数的原因是,当一个接口在蓝图中实现时,关系实际上并没有在编译时建立。因此,C++并不知道接口已经实现,因此我们无法将蓝图类转换为IInterface以直接调用函数。

  20. Execute_函数接受实现接口的对象的指针,并调用一些内部方法来调用所需函数的蓝图实现。

  21. 当您播放级别并四处走动时,自定义的Pawn会不断接收到当其BoxComponent与其他对象重叠时的通知。

  22. 如果它们实现了UTalker/ITalker接口,Pawn 然后尝试在相关的Actor实例上调用StartTalking,然后在屏幕上打印适当的消息。

使用 UInterfaces 实现一个简单的交互系统

本教程将向您展示如何将本章中的一些其他教程组合起来,以演示一个简单的交互系统和一个带有可交互门铃的门,以打开门。

如何操作…

  1. 创建一个新的接口Interactable

  2. 将以下函数添加到IInteractable类声明中:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category=Interactable)
boolCanInteract();
UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = Interactable)
voidPerformInteract();
  1. 在实现文件中为两个函数创建默认实现:
boolIInteractable::CanInteract_Implementation()
{
  return true;
}

voidIInteractable::PerformInteract_Implementation()
{

}
  1. 创建第二个接口Openable

  2. 将此函数添加到其声明中:

UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category=Openable)
void Open();
  1. Interactable一样,为Open函数创建一个默认实现:
voidIOpenable::Open_Implementation()
{
}
  1. 创建一个名为DoorBell的新类,基于StaticMeshActor

  2. DoorBell.h#include "Interactable.h",并在类声明中添加以下函数:

virtual bool CanInteract_Implementation() override;
virtual void PerformInteract_Implementation() override;
UPROPERTY(BlueprintReadWrite, EditAnywhere)
AActor* DoorToOpen;
private:
boolHasBeenPushed;
  1. DoorBell.cpp文件中,#include "Openable.h"

  2. 在构造函数中为我们的DoorBell加载一个静态网格:

HasBeenPushed = false;
autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  //GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
GetStaticMeshComponent()-> SetWorldScale3D(FVector(0.5, 0.5, 0.5));
SetActorEnableCollision(true);

DoorToOpen = nullptr;
  1. 将以下函数实现添加到我们的DoorBell上以实现Interactable接口:
boolADoorBell::CanInteract_Implementation()
{
  return !HasBeenPushed;
}

voidADoorBell::PerformInteract_Implementation()
{
  HasBeenPushed = true;
  if (DoorToOpen->GetClass()->ImplementsInterface(UOpenable::StaticClass()))
  {
    IOpenable::Execute_Open(DoorToOpen);
  }
}
  1. 现在创建一个基于StaticMeshActor的新类,名为Door

  2. 在类头文件中#include OpenableInteractable接口,然后修改Door的声明:

class UE4COOKBOOK_API ADoor : public AStaticMeshActor, public IInteractable, public IOpenable
  1. 将接口函数添加到Door上:
UFUNCTION()
virtual bool CanInteract_Implementation() override { return IInteractable::CanInteract_Implementation(); };
UFUNCTION()
virtual void PerformInteract_Implementation() override;

UFUNCTION()
virtual void Open_Implementation() override;
  1. DoorBell一样,在Door构造函数中,初始化我们的网格组件,并加载一个模型:
autoMeshAsset = ConstructorHelpers::FObjectFinder<UStaticMesh>(TEXT("StaticMesh'/Engine/BasicShapes/Cube.Cube'"));
if (MeshAsset.Object != nullptr)
{
  GetStaticMeshComponent()->SetStaticMesh(MeshAsset.Object);
  //GetStaticMeshComponent()->SetCollisionProfileName(UCollisionProfile::Pawn_ProfileName);
  GetStaticMeshComponent()->bGenerateOverlapEvents = true;
}
GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);
GetStaticMeshComponent()->SetWorldScale3D(FVector(0.3, 2, 3));
SetActorEnableCollision(true);
  1. 实现接口函数:
voidADoor::PerformInteract_Implementation()
{
  GEngine->AddOnScreenDebugMessage(-1, 5, FColor::Red, TEXT("The door refuses to budge. Perhaps there is a hidden switch nearby?"));
}

voidADoor::Open_Implementation()
{
  AddActorLocalOffset(FVector(0, 0, 200));
}
  1. 创建一个基于DefaultPawn的新类,名为AInteractingPawn

  2. 将以下函数添加到Pawn类头文件中:

voidTryInteract();

private:
virtual void SetupPlayerInputComponent(UInputComponent* InInputComponent) override;
  1. Pawn的实现文件中,#include "Interactable.h",然后为头文件中的两个函数提供实现:
voidAInteractingPawn::TryInteract()
{
  APlayerController* MyController = Cast<APlayerController>(Controller);
  if (MyController)
  {
    APlayerCameraManager* MyCameraManager = MyController->PlayerCameraManager;
    autoStartLocation = MyCameraManager->GetCameraLocation();
    autoEndLocation = MyCameraManager->GetCameraLocation() + (MyCameraManager->GetActorForwardVector() * 100);
    FHitResultHitResult;
    GetWorld()->SweepSingleByObjectType(HitResult, StartLocation, EndLocation, FQuat::Identity, 
    FCollisionObjectQueryParams(FCollisionObjectQueryParams::AllObjects),FCollisionShape::MakeSphere(25),
    FCollisionQueryParams(FName("Interaction"),true,this));
    if (HitResult.Actor != nullptr)
    {
      if (HitResult.Actor->GetClass()->ImplementsInterface(UInteractable::StaticClass()))
      {
        if (IInteractable::Execute_CanInteract(HitResult.Actor.Get()))
        {
          IInteractable::Execute_PerformInteract(HitResult.Actor.Get());
        }
      }
    }
  }
}
voidAInteractingPawn::SetupPlayerInputComponent(UInputComponent* InInputComponent)
{
  Super::SetupPlayerInputComponent(InInputComponent);
  InInputComponent->BindAction("Interact", IE_Released, this, &AInteractingPawn::TryInteract);
}
  1. 现在,要么在 C++中创建一个新的GameMode,要么在蓝图中创建一个新的GameMode,并将InteractingPawn设置为我们的默认Pawn类。

  2. DoorDoorbell的副本拖到级别中:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 使用眼滴工具在门铃的Door to Open旁边,如下图所示,然后单击您级别中的门角色实例:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 在编辑器中创建一个名为Interact的新动作绑定,并将其绑定到您选择的一个键:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  5. 播放您的级别,并走到门铃旁。看着它,按下您绑定Interact的键。验证门是否移动一次。

  6. 您还可以直接与门交互以获取有关它的一些信息。

它是如何工作的…

  1. 与以前的教程一样,我们将UFUNCTION标记为BlueprintNativeEventBlueprintCallable,以允许UInterface在本地代码或蓝图中实现,并允许使用任一方法调用函数。

  2. 我们基于StaticMeshActor创建DoorBell以方便起见,并使DoorBell实现Interactable接口。

  3. DoorBell的构造函数中,我们将HasBeenPushedDoorToOpen初始化为默认安全值。

  4. CanInteract的实现中,我们返回HasBeenPushed的反值,以便一旦按钮被按下,就无法进行交互。

  5. PerformInteract中,我们检查是否有一个引用来打开门对象。

  6. 如果我们有一个有效的引用,我们验证门角色是否实现了Openable,然后在我们的门上调用Open函数。

  7. Door中,我们实现了InteractableOpenable,并重写了每个函数。

  8. 我们将DoorCanInteract实现定义为与默认值相同。

  9. PerformInteract中,我们向用户显示一条消息。

  10. Open函数中,我们使用AddActorLocalOffset来将门移动到一定的距离。通过蓝图中的时间轴或线性插值,我们可以使这个过渡变得平滑,而不是瞬间移动。

  11. 最后,我们创建一个新的Pawn,以便玩家实际上可以与物体交互。

  12. 我们创建一个TryInteract函数,并将其绑定到重写的SetupPlayerInputComponent函数中的Interact输入动作。

  13. 这意味着当玩家执行与Interact绑定的输入时,我们的TryInteract函数将运行。

  14. TryInteract获取对PlayerController的引用,将所有 Pawns 都具有的通用控制器引用进行转换。

  15. 通过PlayerController检索PlayerCameraManager,这样我们就可以访问玩家摄像机的当前位置和旋转。

  16. 我们使用摄像机的位置创建起始点和结束点,然后在摄像机位置的前方 100 个单位处,将它们传递给GetWorld::SweepSingleByObjectType函数。

  17. 这个函数接受多个参数。HitResult是一个变量,允许函数返回有关跟踪到的任何对象的信息。CollisionObjectQueryParams允许我们指定我们是否对动态、静态物品或两者都感兴趣。

  18. 我们通过使用MakeSphere函数来完成一个球体跟踪。

  19. 球体跟踪通过定义一个圆柱体来检查物体,而不是一条直线,从而允许稍微有些人为误差。考虑到玩家可能不会完全准确地看着你的物体,你可以根据需要调整球体的半径。

  20. 最后一个参数SweepSingleByObjectType是一个结构体,它给跟踪一个名称,让我们指定是否与复杂的碰撞几何体发生碰撞,最重要的是,它允许我们指定我们要忽略发起跟踪的对象。

  21. 如果HitResult在跟踪完成后包含一个 actor,我们检查该 actor 是否实现了我们的接口,然后尝试调用CanInteract函数。

  22. 如果 actor 表示可以进行交互,我们就告诉它实际执行交互操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值