Skip to content

Decoupling and Actor Component

Caishangqi edited this page Jun 27, 2023 · 5 revisions

以下会总结该项中使用虚幻引擎提供的 蓝图 和 API解耦和设计模式。总得来说,你可以使用虚幻引擎提供的接口类来让你的AActor实现它来完成代码复用和解耦。同时你也可以使用蓝图作为一个Actor的基类来使用其他蓝图内部的逻辑利用原本的蓝图,这里的例子为在制作好基础投射物的蓝图可以使用其他蓝图调用基础投射物蓝图中的组件以及修改各类属性来衍生新的投射物。

基于API接口

Interface的目的是有一些共有的API, 比如说游戏内的箱子可以被打开,你需要一个交互行为又或者是玩家死亡的尸体你希望其他玩家能够与其进行互动,在设置好Interface后其他组件或者实例可以基于实例不同类型来实现接口的方法。

UE编辑器创建C++接口

  • 首先添加一个 Unreal 接口,创建完毕的接口头文件和源文件如下:

头文件

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "DGameplayInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
/*
 *	Interface的目的是有一些共有的API, 比如说游戏内的箱子可以被打开,你需要一个交互行为
 *	又或者是玩家死亡的尸体你希望其他玩家能够与其进行互动
 *
 *	在设置好Interface后其他组件或者实例可以基于实例不同类型来实现接口的方法
 */
class UDGameplayInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 上方的UDGameplayInterface 不应当添加任何东西, 功能性实现在下方 IDGameplayInterface
 * 中, 其中前缀 I 意为接口
 */
class CHARMINGCRAFT_API IDGameplayInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
};

源文件

// Fill out your copyright notice in the Description page of Project Settings.
#include "DGameplayInterface.h"
// Add default functionality here for any IDGameplayInterface functions that are not pure virtual.

其中接口头文件上半部分不应该修改,它包含了UE内置的宏以及基于你的接口生产的UInterface

生成的UInterface

// This class does not need to be modified.
UINTERFACE(MinimalAPI)

class UDGameplayInterface : public UInterface
{
	GENERATED_BODY()
};

编辑接口方法

接下来,你可以按照自己的要求去写一些你想复用的方法,在这个例子中我们想给游戏中的Actor给予交互的功能,因此我写了一个接口方法叫做 void Interact(APawn* InstigatorPawn); 这个方法用于被交互其中 APawn* 类型的参数 InstigatorPawn 代表 煽动者 意思为谁进行的互动。

class CHARMINGCRAFT_API IDGameplayInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	/*!
	 *	@param InstigatorPawn 哪个Pawn激活的这个方法, 通常是玩家
	 */
	UFUNCTION(BlueprintCallable,BlueprintNativeEvent) // UFUNCTION(BlueprintNativeEvent) 允许把这个函数暴露到蓝图中
	void Interact(APawn* InstigatorPawn); //Instigator = Initiator 谁激活的
};

我们把这个接口方法使用 UFUNCTION() 宏暴露给Unreal的资产系统的蓝图编辑器中,其中 BlueprintCallable ,BlueprintNativeEvent 分别代表可以在蓝图中调用此放法以及声明这个接口是C++实现的(Native原生)

实现接口

在这个例子中,我们想让玩家能够交互游戏中的箱子,故我们需要让箱子这个Actor实现该接口

UCLASS()
class CHARMINGCRAFT_API ADItemChest : public AActor, public IDGameplayInterface
{
	GENERATED_BODY()
}

随后在 public: 权限下实现函数原型,这里要注意Unreal的接口实现语法和C++本身语法不一样,语法为 方法名 + "_" + Implementation,我们待实现的方法叫做 Interact() 所以完整如下:

public:
	virtual void Interact_Implementation(APawn* InstigatorPawn) override; //原本接口不需要 _Implementation 但是UE模板要这样

接下来到源代码中实现函数体,这里我们想让 Interact() 函数调用时打开箱子,实现如下:

void ADItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
	/*
	 *	箱子被玩家互动后执行的开箱操作
	 *	SetRelativeRotation 中的Relative是相对于LidMesh 连接的组件这里相对的应该是BaseMesh
	 *	FRotator(TargetPitch,0,0) 对应的是旋转向量旋转向量的仰角为Pitch也就是Y (绿色的)
	 */
	LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
}

创建UActorComponent组件

我们还需要绑定这个交互实现到玩家角色身上来完成解耦工作, UActorComponent 组件可以理解成能附加在任何Actor上的子Actor组件,在Unreal编辑器中新建 ActorComponent类型的类模板并且完成函数原型 void PrimaryInteract();基础交互。

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CHARMINGCRAFT_API UDInteractionComponent : public UActorComponent
{
	GENERATED_BODY()
public:
	
	void PrimaryInteract();
	
public:	
	// Sets default values for this component's properties
	UDInteractionComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

		
};

添加ActorComponent组件

接下来添加这个组件至玩家Actor中,这个组件作为玩家类的成员包含了玩家所拥有的行为

protected:
	UPROPERTY(VisibleAnywhere)
	UDInteractionComponent* InteractionComp;

在玩家类源代码中的构造器里初始化这个 UDInteractionComponent 对象

ADCharacter::ADCharacter()
{
	// 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;
	InteractionComp = CreateDefaultSubobject<UDInteractionComponent>("InteractionComp");
}

玩家类交互函数

接下来给玩家类中创建一个交互函数,在这个交互函数的方法体中调用 InteractionComp 中的 PrimaryInteract() 方法这样允许我们在玩家类中接受控制器输入方法SetupPlayerInputComponent()中绑定交互键至函数。

头文件

protected:
	virtual void PrimaryInteract();

源文件

void ADCharacter::PrimaryInteract()
{
	InteractionComp->PrimaryInteract();
}

绑定代码如下:

void ADCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
        PlayerInputComponent->BindAction("PrimaryInteract", IE_Pressed, this, &ADCharacter::PrimaryInteract);
}

实现ActorComponent组件中的方法

接下来要实现 ActorComponent 组件中的方法来调用箱子上的接口函数

void UDInteractionComponent::PrimaryInteract()
{
	/*!
	 *	LineTrace从世界发射一个虚拟线,到达目的地,检索穿过的所有目标
	 *	FHitResult Hit 命中结果集
	 */

	FCollisionObjectQueryParams ObjectQueryParams;
	ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic); //搜索到WorldDynamic类型的目标就停止

	//GetOwner() 直接返回这个ActorComponent绑定的Actor,这里是我们的角色
	AActor* MyOwner = GetOwner();

	FVector EyeLocation;
	FRotator EyeRotation;

	MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
	FVector End = EyeLocation + (EyeRotation.Vector() * 1000);

	//是否击中,这个也可以不需要返回值也能正常运行
	FHitResult Hit;
	bool bBlockingHit = GetWorld()->LineTraceSingleByObjectType(Hit, EyeLocation, End, ObjectQueryParams);

	float Radius = 30.0f;
	TArray<FHitResult> Hits;
	FCollisionShape Shape;
	Shape.SetSphere(Radius);
	bool bBlockingHit = GetWorld()->SweepMultiByObjectType(Hits, EyeLocation, End, FQuat::Identity, ObjectQueryParams,
	                                                       Shape);
	FColor LineColor = bBlockingHit ? FColor::Green : FColor::Red;
	for (FHitResult Hit : Hits)
	{
		AActor* HitActor = Hit.GetActor();
		if (HitActor)
		{
			if (HitActor->Implements<UDGameplayInterface>()) //注意 Check Implements 泛型是UDGameplayInterface, UE生成的接口
			{
				APawn* MyPawn = Cast<APawn>(MyOwner);
				IDGameplayInterface::Execute_Interact(HitActor, MyPawn); // 注意 调用的时候是使用的我们自己写的接口而不是 UE生产的
				break; //碰到第一个可交互物品就跳出循环
			}

		}
	}

}

由于交互方法实现可以有很多种,这里使用的是球状追踪,即为以玩家控制器中心的视角转向朝游戏世界发射一条轨迹, 随后创建一个虚拟球体去按照这个轨迹滚动将撞到的Actor或者是你指定的过滤列表中的目标收集到 TArray<FHitResult> Hits 集合中。

  • 遍历整个集合拿到每次遍历的 FHitResult Hit
  • 使用 Hit.GetActor() 来获取击中的 Actor
  • 空指针安全判断,使用if。如果是Actor则进入下一个if
  • HitActor->Implements<UDGameplayInterface>() 这是UE提供的Actor上的方法,这个方法可以返回当前Actor是否实现了某个接口, 注意泛型 后面要填写的 是UE生成的 接口(U开头 UDGameplayInterface)而不是你自己写的接口 IDGameplayInterface
  • 其中 ActorComponent 中还有一个方法叫做 GetOwner(), 这个方法返回该组件的拥有者,这里为玩家,我们使用 AActor* MyOwner = GetOwner() 获取玩家Actor指针
  • 接下来从这个被射线击中的Actor中调用这个Actor上面实现的方法,还记得我们的 Interact() 方法参数是 APawn* 类型,我们需要使用Unreal的安全类型转换 APawn* MyPawn = Cast<APawn>(MyOwner)
  • 调用该方法传入参数 IDGameplayInterface::Execute_Interact(HitActor, MyPawn),注意这里要使用我们自己的接口(I开头 IDGameplayInterface)而 不是UE生成的 。执行接口方法需要传入实现接口的类的实例对象故把类型为 ADItemChest 的 HitActor 传入方法,同时方法本身需要传入APawn* 类型。
  • 我们不想一次打开所有射线经过的箱子,所以当执行完第一个互动函数后就可以跳出循环了。

说明 I

中间的数学和旋转获取以及线性追踪的方法参数和说明不在此区块进行说明。

基于蓝图

Clone this wiki locally