目录
首先说明,课程中在这部分的做法可能不太容易理解,因为涉及到一部分设计模式和C++多态的知识(个人觉得很类似),对这些知识感到生疏的朋友可以在看完本节后再自行了解一下。
1. 创建箱子和UI类
在这节内容,需要实现角色按下键盘E来打开物品箱。首先在UE中创建一个SurGameplayInterface类,继承自Unreal接口类,会发现.h文件中生成了两个类:USurGameplayInterface和ISurGameplayInterface。根据代码注释,第一个类不应该被修改,相关功能需要添加到第二个类中。这个类的作用是作为共享的公共接口,具体实现需要其他类来重写(感觉类似于虚函数)。
具体的实现方式, 是使用UFUNCTION宏来修饰我们自己编写的Interact函数,使其可以在UE蓝图中使用和编辑。同时设置这个函数的输入,可以传入不同的APawn对象(调用这个函数的主体)来方便我们控制相关动画的显示。相关的UFUNCTION用法有:
BlueprintCallable | 可在蓝图中调用 |
BlueprintImplementableEvent | 可在蓝图中实现 |
BlueprintNativeEvent | 蓝图可调用可实现;需要被重写,但也有默认实现 |
// SurGameplayInterface.h
class SURKEAUE_API ISurGameplayInterface
{
public:
// 传入调用者。为了使不能双足行走的角色能正确调用,定义为Pawn而不是Character
UFUNCTION(BlueprintNativeEvent)
void Interact(APawn* InstigatorPawn);
};
然后,从AActor和ISurGameplayInterface派生一个SurItemChest箱子类,并添加两个Mesh控件,分别表示箱子的底座和盖子。因为给Interact()设置了UFUNCTION(BlueprintNativeEvent),在UE中规定了需要使用如下语法来实现(重写?)这个函数。根据官方文档的说明,这种用法很类似C++中多态的实现。
// SurItemChest.h
class SURKEAUE_API ASurItemChest : public AActor, public ISurGameplayInterface
{
public:
// UFUNCTION(BlueprintNativeEvent)修饰后必须添加_Implementation
void Interact_Implementation(APawn* InstigatorPawn);
protected:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* LidMesh;
};
// SurItemChest.cpp
ASurItemChest::ASurItemChest()
{
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMesh");
RootComponent = BaseMesh;
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>("LidMesh");
LidMesh->SetupAttachment(BaseMesh);
}
2. 创建蓝图类
在UE中创建一个SurItemChest的蓝图类箱子,命名为TreasureChest。在课程项目提供的ExampleContent文件夹中有箱子的网格体,将其分别设置给TreasureChest的Base和Lid即可。
然后通过“变换”属性调整一下盖子的位置,使其刚好贴合在底座的上方。
3. 控制箱子打开
我们可以在视口中试验一下,拖拽调整盖子的角度就可以实现箱子的开合效果,调整时可以注意细节面板中“变换” -> “旋转”属性的变化,发现是Pitch在改变。
因此,只要通过改变Pitch变量就可以实现箱子的开合动画。此外,为了更方便的控制打开的角度,在.h中声明了浮点型TargetPitch并使用UPROPEERTY(EditAnywhere)宏修饰,然后在.cpp构造函数中赋初值。
void ASurItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
// 相对base进行旋转,参数(pitch, yaw, roll)
LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
}
课程中也提到,这个方法实现的动画比较生硬。但目前的重心不在制作动画,后续会使用Tick函数实现更加精确丝滑的动画控制。
4. 控制动画
要实现开箱动画的控制,首先需要绑定按键事件,按下后执行某个函数,这个函数可以判断视线内一定距离内是否有箱子,有的话就将箱子打开。
根据设计模式的相关理论,开发时要尽量降低各个功能模块的耦合性,从而避免后期代码的臃肿冗余。因此在实现这个功能时,就不继续在SurCharacter类中编写具体代码,而是创建一个类来专门负责实现这部分的逻辑,然后将其与SurCharacter类组合即可。同时课程中也提到,因为所有角色都可以进行攻击,之前实现的攻击的相关代码最好也单独封装提供调用,这在后续会进行优化。
要实现这个功能,可以使用UE中的ActorComponent类。顾名思义,这个类可以像普通的Component一样附加到Actor上。因此派生出SurInteractionComponent类,我们需要在其中实现检查周围有哪些物体可以互动,即碰撞查询(collision query),所以在.h中声明PrimaryInteract()来实现这个功能需求。
// SurInteractionComponent.h
class SURKEAUE_API USurInteractionComponent : public UActorComponent
{public:
void PrimaryInteract();
};
然后在SurCharacter的两个文件中声明和创建SurInteractionComponent的实例,顺便再声明一下将要绑定的按键操作PrimaryInteract。
// SurCharacter.h
UCLASS()
class SURKEAUE_API ASurCharacter : public ACharacter
{
protected:
// 界面
UPROPERTY(VisibleAnywhere)
USurInteractionComponent* InteractionComp;
void PrimaryInteract();
}
// SurCharacter.cpp
ASurCharacter::ASurCharacter()
{
InteractionComp = CreateDefaultSubobject<USurInteractionComponent>("InteractionComp");
}
void ASurCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 交互
PlayerInputComponent->BindAction("PrimaryInteract", IE_Pressed, this, &ASurCharacter::PrimaryInteract);
}
void ASurCharacter::PrimaryInteract() {
InteractionComp->PrimaryInteract();
}
要实现碰撞检测,游戏开发中常用发射射线的方法,即从我们角色的眼镜发出一定长度的射线,当射线碰撞到第一个物体后在函数中返回这个对象。在UE中LineTraceSingleByObjectType()函数可以实现这个功能,其四个参数分别为:检测结果、射线起点、射线终点、检测参数。关于碰撞的相关内容,可以参考官方文档:
void USurInteractionComponent::PrimaryInteract()
{
FHitResult Hit; // 检测结果
FVector EyeLocation; // 角色眼睛位置
FRotator EyeRotation; // 角色视线方向
AActor* MyOwner = GetOwner(); // 获取控制角色
// 将玩家视线的位置和方向输出到EyeLocation和EyeRotation
MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
// 沿着视线方向,模型的眼睛位置开始1000cm距离的点为终点
FVector End = EyeLocation + (EyeRotation.Vector() * 1000);
FCollisionObjectQueryParams ObjectQueryParams; // 查询参数
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic); // 选择查询场景动态对象
GetWorld()->LineTraceSingleByObjectType(Hit, EyeLocation, End, ObjectQueryParams);
}
最后是根据碰撞结果来调用打开箱子的函数,细节已经在注释中说明:
// 从判断结果中获取检测到的Actor,没检测到则为空
AActor* HitActor = Hit.GetActor();
if (HitActor) {
// 如果检测到actor不为空,再判断actor有没有实现SurGameplayInterface类
if (HitActor->Implements<USurGameplayInterface>()) {
// 我们定义的Interact()传入为Pawn类型,因此做类型转换
APawn* MyPawn = Cast<APawn>(MyOwner);
// 多态,根据传入的HitActor调用相应函数
// 第一个参数不能为空,所以外层已经判空;第二个参数是我们自定义的,暂时没有影响,可以不判空
ISurGameplayInterface::Execute_Interact(HitActor, MyPawn);
// 用于debug,绘制这条碰撞检测的线,绿色
DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Green, false, 3);
}
}
else{ DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Red, false, 3); }
最后在UE中绑定键盘操作,然后测试代码效果,发现角色已经可以成功打开箱子了。
在本节课程的最后,作者还提出了对碰撞查询的优化。对于箱子这样有一定体积的物体使用射线检测无可厚非,但若是要实现捡硬币之类的小物品,这种方法对视角的要求就太过苛刻了。所以UE中还存在各种检测方法,如扫射、球体检测等等,开发者应该根据实际情况来选择最优的检测方法。
5. 完整代码
SurCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SurCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class USurInteractionComponent;
UCLASS()
class SURKEAUE_API ASurCharacter : public ACharacter
{
GENERATED_BODY()
public:
ASurCharacter();
protected:
//弹簧臂组件
UPROPERTY(VisibleAnywhere)
USpringArmComponent* SpringArmComp;
//相机组件
UPROPERTY(VisibleAnywhere)
UCameraComponent* CameraComp;
// 投射体子类
UPROPERTY(EditAnywhere)
TSubclassOf<AActor> ProjectileClass;
// 界面
UPROPERTY(VisibleAnywhere)
USurInteractionComponent* InteractionComp;
virtual void BeginPlay() override;
void MoveForward(float value);
void MoveRight(float value);
void PrimaryAttack();
void PrimaryInteract();
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
SurCharacter.cpp
#include "SurCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "SurInteractionComponent.h"
ASurCharacter::ASurCharacter()
{
PrimaryActorTick.bCanEverTick = true;
SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("SpringArmComp");
SpringArmComp->bUsePawnControlRotation = true;
SpringArmComp->SetupAttachment(RootComponent);
CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
CameraComp->SetupAttachment(SpringArmComp);
InteractionComp = CreateDefaultSubobject<USurInteractionComponent>("InteractionComp");
GetCharacterMovement()->bOrientRotationToMovement = true;
bUseControllerRotationYaw = false;
}
void ASurCharacter::BeginPlay()
{
Super::BeginPlay();
}
void ASurCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void ASurCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 移动控制
PlayerInputComponent->BindAxis("MoveForward", this, &ASurCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &ASurCharacter::MoveRight);
PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
// 跳跃
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
// 攻击
PlayerInputComponent->BindAction("PrimaryAttack", IE_Pressed, this, &ASurCharacter::PrimaryAttack);
// 交互
PlayerInputComponent->BindAction("PrimaryInteract", IE_Pressed, this, &ASurCharacter::PrimaryInteract);
}
// 角色向前移动
void ASurCharacter::MoveForward(float value)
{
FRotator ControlRot = GetControlRotation();
// 转向只关注水平Yaw方向,因此置0防止影响
ControlRot.Pitch = 0;
ControlRot.Roll = 0;
// 获取相机(鼠标控制器)的朝向,并朝这个方向移动
AddMovementInput(ControlRot.Vector(), value);
}
// 角色向右移动
void ASurCharacter::MoveRight(float value)
{
FRotator ControlRot = GetControlRotation();
ControlRot.Pitch = 0;
ControlRot.Roll = 0;
// 获取相机(鼠标控制器)的朝向,转向右侧,并朝这个方向移动
FVector RightVector = FRotationMatrix(ControlRot).GetScaledAxis(EAxis::Y);
AddMovementInput(RightVector, value);
}
// 左键攻击
void ASurCharacter::PrimaryAttack() {
// 获取模型右手位置
FVector RightHandLoc = GetMesh()->GetSocketLocation("Muzzle_01");
// 朝向角色方向,在角色的右手位置生成
FTransform SpawnTM = FTransform(GetActorRotation(), RightHandLoc);
// 此处设置碰撞检测规则为:即使碰撞也总是生成
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
GetWorld()->SpawnActor<AActor>(ProjectileClass, SpawnTM, SpawnParams);
}
// 交互
void ASurCharacter::PrimaryInteract() {
InteractionComp->PrimaryInteract();
}
SurItemChest.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "SurGameplayInterface.h"
#include "SurItemChest.generated.h"
UCLASS()
class SURKEAUE_API ASurItemChest : public AActor, public ISurGameplayInterface
{
GENERATED_BODY()
protected:
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* BaseMesh;
UPROPERTY(VisibleAnywhere)
UStaticMeshComponent* LidMesh;
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
UPROPERTY(EditAnywhere)
float TargetPitch;
// UFUNCTION(BlueprintNativeEvent)修饰后必须添加_Implementation
void Interact_Implementation(APawn* InstigatorPawn);
ASurItemChest();
};
SurItemChest.cpp
#include "SurItemChest.h"
ASurItemChest::ASurItemChest()
{
PrimaryActorTick.bCanEverTick = true;
BaseMesh = CreateDefaultSubobject<UStaticMeshComponent>("BaseMesh");
RootComponent = BaseMesh;
LidMesh = CreateDefaultSubobject<UStaticMeshComponent>("LidMesh");
LidMesh->SetupAttachment(BaseMesh);
TargetPitch = 110.0f;
}
void ASurItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
// 相对base进行旋转,参数(pitch, yaw, roll)
LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
}
void ASurItemChest::BeginPlay()
{
Super::BeginPlay();
}
void ASurItemChest::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
SurGameplayInterface.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SurGameplayInterface.generated.h"
UINTERFACE(MinimalAPI)
class USurGameplayInterface : public UInterface
{
GENERATED_BODY()
};
class SURKEAUE_API ISurGameplayInterface
{
GENERATED_BODY()
public:
// 传入调用者。为了使不能双足行走的角色能正确调用,定义为Pawn而不是Character
UFUNCTION(BlueprintNativeEvent)
void Interact(APawn* InstigatorPawn);
};
SurGameplayInterface.cpp为空
SurInteractionComponent.h
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "SurInteractionComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SURKEAUE_API USurInteractionComponent : public UActorComponent
{
GENERATED_BODY()
public:
void PrimaryInteract();
USurInteractionComponent();
protected:
virtual void BeginPlay() override;
public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
};
SurInteractionComponent.cpp
#include "SurInteractionComponent.h"
#include "SurGameplayInterface.h"
#include "DrawDebugHelpers.h"
USurInteractionComponent::USurInteractionComponent()
{
PrimaryComponentTick.bCanEverTick = true;
}
void USurInteractionComponent::BeginPlay()
{
Super::BeginPlay();
}
void USurInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
}
void USurInteractionComponent::PrimaryInteract()
{
FHitResult Hit; // 检测结果
FVector EyeLocation; // 角色眼睛位置
FRotator EyeRotation; // 角色视线方向
AActor* MyOwner = GetOwner(); // 获取控制角色
// 将玩家视线的位置和方向输出到EyeLocation和EyeRotation
MyOwner->GetActorEyesViewPoint(EyeLocation, EyeRotation);
// 沿着视线方向,模型的眼睛位置开始1000cm距离的点为终点
FVector End = EyeLocation + (EyeRotation.Vector() * 1000);
FCollisionObjectQueryParams ObjectQueryParams; // 查询参数
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic); // 选择查询场景动态对象
GetWorld()->LineTraceSingleByObjectType(Hit, EyeLocation, End, ObjectQueryParams);
// 从判断结果中获取检测到的Actor,没检测到则为空
AActor* HitActor = Hit.GetActor();
if (HitActor) {
// 如果检测到actor不为空,再判断actor有没有实现SurGameplayInterface类
if (HitActor->Implements<USurGameplayInterface>()) {
// 我们定义的Interact()传入为Pawn类型,因此做类型转换
APawn* MyPawn = Cast<APawn>(MyOwner);
// 多态,根据传入的HitActor调用相应函数
// 第一个参数不能为空,所以外层已经判空;第二个参数是我们自定义的,暂时没有影响,可以不判空
ISurGameplayInterface::Execute_Interact(HitActor, MyPawn);
// 用于debug,绘制这条碰撞检测的线,绿色
DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Green, false, 3);
}
}
else { DrawDebugLine(GetWorld(), EyeLocation, End, FColor::Red, false, 3); }
}