斯坦福UE4 + C++课程学习记录 7:打开箱子

目录

1. 创建箱子和UI类

2. 创建蓝图类

3. 控制箱子打开

4. 控制动画

5. 完整代码


        首先说明,课程中在这部分的做法可能不太容易理解,因为涉及到一部分设计模式和C++多态的知识(个人觉得很类似),对这些知识感到生疏的朋友可以在看完本节后再自行了解一下。

1. 创建箱子和UI类

        在这节内容,需要实现角色按下键盘E来打开物品箱。首先在UE中创建一个SurGameplayInterface类,继承自Unreal接口类,会发现.h文件中生成了两个类:USurGameplayInterface和ISurGameplayInterface。根据代码注释,第一个类不应该被修改,相关功能需要添加到第二个类中。这个类的作用是作为共享的公共接口,具体实现需要其他类来重写(感觉类似于虚函数)。

        具体的实现方式, 是使用UFUNCTION宏来修饰我们自己编写的Interact函数,使其可以在UE蓝图中使用和编辑。同时设置这个函数的输入,可以传入不同的APawn对象(调用这个函数的主体)来方便我们控制相关动画的显示。相关的UFUNCTION用法有:

表7-1 常用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即可。

图7-1 设置网格体

        然后通过“变换”属性调整一下盖子的位置,使其刚好贴合在底座的上方。

图7-2 调整网格体位置

 3. 控制箱子打开

        我们可以在视口中试验一下,拖拽调整盖子的角度就可以实现箱子的开合效果,调整时可以注意细节面板中“变换” -> “旋转”属性的变化,发现是Pitch在改变。

图7-3 网格体旋转

        因此,只要通过改变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中绑定键盘操作,然后测试代码效果,发现角色已经可以成功打开箱子了。

图7-4 运行结果

        在本节课程的最后,作者还提出了对碰撞查询的优化。对于箱子这样有一定体积的物体使用射线检测无可厚非,但若是要实现捡硬币之类的小物品,这种方法对视角的要求就太过苛刻了。所以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); }
	
}

猜你喜欢

转载自blog.csdn.net/surkea/article/details/127122458