Stanford UE4 + C++ course study record 7: open the box

Table of contents

1. Create boxes and UI classes

2. Create a blueprint class

3. Control the box to open

4. Control animation

5. Complete code


        First of all, the practice in this part of the course may not be easy to understand, because it involves a part of the knowledge of design patterns and C++ polymorphism (personally thinks it is very similar), friends who are unfamiliar with these knowledge can read this section later Find out for yourself.

1. Create boxes and UI classes

        In this section, it is necessary to realize that the character presses the keyboard E to open the item box. First create a SurGameplayInterface class in UE, which inherits from the Unreal interface class, and you will find that two classes are generated in the .h file: U SurGameplayInterface and I SurGameplayInterface. According to the code comments, the first class should not be modified, and the relevant functionality needs to be added to the second class. The role of this class is to serve as a shared public interface, and the specific implementation needs to be rewritten by other classes (it feels similar to a virtual function).

        The specific implementation method is to use the UFUNCTION macro to modify the Interact function written by ourselves, so that it can be used and edited in the UE blueprint. At the same time, set the input of this function, and you can pass in different APawn objects (the main body that calls this function) to facilitate us to control the display of related animations. Related UFUNCTION usages are:

Table 7-1 Commonly used UFUNCTION macros
BlueprintCallable Can be called in blueprint
BlueprintImplementableEvent Can be implemented in blueprint
BlueprintNativeEvent Blueprints are callable and implementable; need to be overridden, but also have default implementations
// SurGameplayInterface.h
class SURKEAUE_API ISurGameplayInterface
{
public:
// 传入调用者。为了使不能双足行走的角色能正确调用,定义为Pawn而不是Character
	UFUNCTION(BlueprintNativeEvent)
	void Interact(APawn* InstigatorPawn);
};

        Then, derive a SurItemChest chest class from AActor and ISurGameplayInterface, and add two Mesh controls to represent the base and lid of the chest, respectively. Because UFUNCTION(BlueprintNativeEvent) is set for Interact(), it is stipulated in UE that the following syntax needs to be used to implement (rewrite?) this function. According to the official documentation, this usage is very similar to the implementation of polymorphism in 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. Create a blueprint class

        Create a SurItemChest blueprint class box in UE and name it TreasureChest. There is a box mesh in the ExampleContent folder provided by the course project, which can be set to the Base and Lid of TreasureChest respectively.

Figure 7-1 Setting up the mesh body

        Then adjust the position of the cover through the "Transform" property so that it fits just above the base.

Figure 7-2 Adjusting the position of the mesh body

 3. Control the box to open

        We can experiment in the viewport, drag and adjust the angle of the lid to realize the opening and closing effect of the box. When adjusting, we can pay attention to the change of the "Transform" -> "Rotation" attribute in the details panel, and find that the Pitch is changing.

Figure 7-3 Mesh rotation

        Therefore, the opening and closing animation of the box can be realized only by changing the Pitch variable. In addition, in order to control the opening angle more conveniently, the floating-point TargetPitch is declared in .h and modified with the UPROPEERTY (EditAnywhere) macro, and then the initial value is assigned in the .cpp constructor.

void ASurItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
	// 相对base进行旋转,参数(pitch, yaw, roll)
	LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
}

         It is also mentioned in the course that the animation achieved by this method is relatively blunt. But the current focus is not on making animations, and the Tick function will be used in the future to achieve more precise and silky animation control.


4. Control animation

        To realize the control of the unboxing animation, you first need to bind the button event, and execute a function after pressing it. This function can determine whether there is a box within a certain distance of the line of sight, and if so, open the box.

        According to the relevant theories of design patterns, the coupling of each functional module should be reduced as much as possible during development, so as to avoid bloated and redundant code in the later stage. Therefore, when implementing this function, instead of continuing to write specific codes in the SurCharacter class, create a class to be responsible for implementing this part of the logic, and then combine it with the SurCharacter class. At the same time, it is also mentioned in the course that because all characters can attack, it is best to package and call the relevant code of the previously implemented attack separately, which will be optimized in the future.

To achieve this function, you can use the ActorComponent class         in UE . As the name suggests, this class can be attached to an Actor like a normal Component. Therefore, the SurInteractionComponent class is derived, and we need to check which objects around can interact, that is, the collision query (collision query), so declare PrimaryInteract() in .h to achieve this functional requirement.

// SurInteractionComponent.h
class SURKEAUE_API USurInteractionComponent : public UActorComponent
{public:	
	void PrimaryInteract();
};

         Then declare and create an instance of SurInteractionComponent in the two files of SurCharacter, and declare the PrimaryInteract of the key operation to be bound by the way.

// 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();
}

        To achieve collision detection, the method of emitting rays is commonly used in game development, that is, a ray of a certain length is emitted from the glasses of our character, and when the ray hits the first object, this object is returned in the function. The LineTraceSingleByObjectType() function in UE can realize this function, and its four parameters are: detection result, ray start point, ray end point, and detection parameters. For more information about collisions, please refer to the official documentation :

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);
}

         Finally, the function to open the box is called according to the collision result. The details have been explained in the comments:

	// 从判断结果中获取检测到的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); }

         Finally, bind the keyboard operation in UE, and then test the code effect, and found that the character can successfully open the box.

Figure 7-4 Running results

        At the end of this section of the course, the author also proposes the optimization of collision queries. It is understandable to use ray detection for objects with a certain volume such as boxes, but if you want to realize small items such as coins, this method has too strict requirements on the viewing angle. Therefore, there are still various detection methods in UE, such as sweeping, sphere detection, etc., and developers should choose the optimal detection method according to the actual situation.


 5. Complete code

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 is empty

AboutInteractionComponent.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;

		
};

AboutInteractionComponent.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); }
	
}

Guess you like

Origin blog.csdn.net/surkea/article/details/127122458