4 - 蓝图函数库,Actor组件和接口
本文将初步介绍UE5中有关蓝图函数库,Actor组件和用户接口等概念和应用,让项目变得更加松耦合。
松耦合思想
把许多逻辑写到一个Actor中后不容易维护,且代码会出现冗余。这时候需要“松耦合”的思想帮忙,将一些通用的逻辑封装到一起,容易管理和使用。
在UE中,能帮助我们实现“松耦合”的工具有三个:
- 蓝图函数库类 BlueprintFunctionLibrary:将项目中一些通用的函数从Actor中移出来,以静态函数的形式封装到此类中。
- Actor组件 ActorComponent:将一些Actor共用的逻辑封装成一个Actor组件中,并将其附加到原Actor中。
- 接口 Unreal Interface:和Actor组件的思想相同,能使我们的项目结构更合理,组织更有序。
蓝图函数库
在UE5中,有一个BlueprintFunctionLibrary
类,该类包含了一组不属于任何特定Actor的静态函数集合,可以在项目的多个部分中使用。例如之前使用的Kismet
库,其中的GameplayStatices
对象、KismetMathLibrary
和KismetSystemLibrary
都是蓝图函数库。
接下来我们将新建一个蓝图函数库,实现CanSeeActor()
函数,该函数根据给定Actor的位置和目标Actor,判断给定Actor能否看见目标Actor(有障碍物就看不见)。
首先,新建一个基于BlueprintFunctionLibrary
类的DodgeballFunctionLibrary
类,并声明CanSeeActor()
:
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DodgeballFunctionLibrary.generated.h"
/**
* 给Dodgeball项目使用的工具函数集合
*/
UCLASS()
class DODGEBALL_API UDodgeballFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// 在所给位置中能否看见所给Actor
static bool CanSeeActor(const UWorld* World, FVector Location, const AActor* TargetActor, TArray<const AActor*> IgnoreActors = TArray<const AActor*>());
};
然后在源文件实现就行了:
#include "DodgeballFunctionLibrary.h"
#include "Engine/World.h"
#include "CollisionQueryParams.h"
#include "DrawDebugHelpers.h"
bool UDodgeballFunctionLibrary::CanSeeActor(const UWorld* World, FVector Location, const AActor* TargetActor, TArray<const AActor*> IgnoreActors)
{
if (TargetActor == nullptr)
{
return false;
}
// 射线检测
FHitResult HitResult;
FVector Start = Location;
FVector End = TargetActor->GetActorLocation();
ECollisionChannel Channel = ECC_GameTraceChannel1;
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActors(IgnoreActors);
World->LineTraceSingleByChannel(HitResult, Start, End, Channel, QueryParams);
// Debug: 可视化射线检测
DrawDebugLine(World, Start, End, FColor::Red);
return !HitResult.bBlockingHit;
}
Actor组件
Actor是在UE5中创建逻辑的主要方式,一个Actor可以包含几个Actor组件。Actor组件是可以添加到Actor中的对象,并且可以负责多种功能,例如负责库存系统、或者提供飞行能力。Actor组件必须始终属于并存储于Actor中,这个Actor被称为这些组件的所有者(Owner)。
常用的Actor组件有如下种类:
- 纯代码Actor组件:在Actor内部作为自己的类,有自己的属性和方法,既可以和Owner交互,也可以被其他Actor交互。
- 网格体组件:用于渲染多种类型的网格体对象(如静态网格体,骨骼网格体等)。
- 碰撞组件:用于接收和生成碰撞事件。
- 摄像机组件。
在项目中添加逻辑时,如果可以将其封装在单独的组件中,从而实现松耦合,那就应该这样做。接下来我们创建一个负责处理Actor生命值的HealthComponent
。
创建Actor组件
首先,创建一个基于ActorComponent
类的C++类HealthComponent
:
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "HealthComponent.generated.h"
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class DODGEBALL_API UHealthComponent : public UActorComponent
{
GENERATED_BODY()
protected:
UPROPERTY(EditDefaultsOnly, Category = Health)
float Health = 100.0f;
public:
// Sets default values for this component's properties
UHealthComponent();
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
void LoseHealth(float Amount);
protected:
// Called when the game starts
virtual void BeginPlay() override;
};
接下来实现LoseHealth()
逻辑:
void UHealthComponent::LoseHealth(float Amount)
{
Health -= Amount;
// 生命值低于0就执行OnDeath()
if (Health <= 0.f)
{
Health = 0.f;
// todo
}
}
集成Actor组件
然后就能将其集成到想要拥有生命值的角色了,例如玩家类:
// .h
UHealthComponent* HealthComponent;
// 构造函数
{
...
// 初始化Health组件
HealthComponent = CreateDefaultSubobject<UHealthComponent>(TEXT("Health Component"));
}
访问Actor组件
接下来在躲避球类中通过玩家Actor来访问该Actor组件,用于扣除接触到的玩家血量:
void ADodgeballProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector ImpulseNormal, const FHitResult& HitResult)
{
// 击中玩家后的逻辑:
// 1. 获取它的HealthComponent, 进行扣血
// 2. 销毁自身
ADodgeballCharacter* Player = Cast<ADodgeballCharacter>(OtherActor);
if (Player != nullptr)
{
UHealthComponent* HealthComponent = Player->FindComponentByClass<UHealthComponent>();
if (HealthComponent != nullptr)
{
HealthComponent->LoseHealth(Damage);
}
Destroy();
}
}
其中,FindComponentByClass<>()
将返回该Actor类中特定Actor组件的指针,如果没有这个Actor组件,会返回nullptr
。此外,GetComponents()
将返回该Actor中所有Actor组件的列表。
接口
上面的LoseHealth()
还没写完,由于每个Owner死亡时的处理可能不同,我们需要让它调用Owner自己的OnDeath()
回调,可以通过接口(Interface)实现。接口是包含函数集合的类,如果对象实现该接口,则该函数功能集合必须被对象实现。
蓝图本地事件
通过对UFUNCTION()
宏添加BlueprintNativeEvent
标记,函数可以转换为蓝图本地事件。蓝图本地事件就是,在C++中实现默认函数体,然后在蓝图中可选重载。
蓝图本地事件的声明如下:
UFUNCTION(BlueprintNativeEvent)
void MyEvent();
virtual void MyEvent_Implementation();
其中,第一个函数是蓝图签名,它允许我们在蓝图中重写事件,而第二个函数是C++签名,它允许我们在C++中重写事件,第二行不能随意改名,格式必须是第一行函数名_Implementation()
。
此外,除了BlueprintNativeEvent
标记外,还有如下两个标记:
BlueprintCallable
:C++ 中实现函数,蓝图调用;BlueprintImplementableEvent
:蓝图中实现函数,C++ 声明和调用;
声明接口
接下来就能声明OnDeath()
接口了,首先创建一个基于Unreal Interface
类的HealthInterface
类,然后声明接口:
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "HealthInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UHealthInterface : public UInterface
{
GENERATED_BODY()
};
/**
* HealthComponent相关接口
*/
class DODGEBALL_API IHealthInterface
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
// 人物生命值 <= 0后调用的回调
UFUNCTION(BlueprintNativeEvent, Category = Health)
void OnDeath();
virtual void OnDeath_Implementation() = 0;
};
然后在玩家类上实现接口:
// .h
class ADodgeballCharacter : public ACharacter, public IHealthInterface
{
//...
virtual void OnDeath_Implementation() override;
}
// .cpp
void ADodgeballCharacter::OnDeath_Implementation()
{
// 死亡就退出游戏
UKismetSystemLibrary::QuitGame(this, nullptr, EQuitPreference::Quit, true);
}
最后在HealthComponent中调用所属Actor的OnDeath()
接口:
void UHealthComponent::LoseHealth(float Amount)
{
Health -= Amount;
// 生命值低于0就执行OnDeath()
if (Health <= 0.f)
{
Health = 0.f;
// 看看该组件所属对象是否实现了OnDeath()
if (GetOwner()->Implements<UHealthInterface>())
{
IHealthInterface::Execute_OnDeath(GetOwner());
}
}
}
其中,先调用Implements<UHealthInterface>()
查看所属Actor是否实现了此接口,然后就能让接口执行函数了,它的格式是接口::Execute_函数名(GetOwner(), 参数列表)
。
参考资料
中文翻译:《UE5 C++ 游戏开发完全学习教程》
英文原版:《Elevating Game Experiences with UE5》