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

一、创建箱子类和UI类

  1. 需要实现角色按下键盘E来与宝箱交互,可打开可关闭。
  2. 首先在UE中创建一个SurGameplayInterface类,继承自Unreal接口类,会发现.h文件中生成了两个类:USurGameplayInterface和ISurGameplayInterface。根据代码注释,第一个类不应该被修改,相关功能需要添加到第二个类中。这个类的作用是作为共享的公共接口,具体实现需要其他类来重写。

在UE中,接口实际上分为两个部分:
U接口类:是一个UObject类,主要用于让引擎知道这个接口的存在,并管理接口的元数据。由引擎生成和管理的,修改这个部分可能会破坏引擎的正常运行。这个类通常包含静态方法属性
I接口类:是一个纯虚类,包含接口定义的方法。这些方法是需要在实现这个接口的类中覆盖的。

  1. 相关的UFUNCTION用法:
    (1) BlueprintCallable:可在蓝图中调用
    (2)BlueprintImplementableEvent:可在蓝图中实现
    (3)BlueprintNativeEvent:蓝图可调用可实现;需要被重写,但也有默认实现
// SurGameplayInterface.h
#pragma once

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

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USGameplayInterface : public UInterface
{
	GENERATED_BODY()
};

class ACTIONROGUELIKE_API ISGameplayInterface
{
	GENERATED_BODY()
public:
	// 传入调用者。为了使不能双足行走的角色能正确调用,定义为Pawn而不是Character 
	UFUNCTION(BlueprintNativeEvent)
	void Interact(APawn* InstigatorPawn);
};
  1. AActorISurGameplayInterface必须要继承自)派生一个SurItemChest箱子类,并添加两个Mesh控件(底座和盖子)。因为给Interact()设置了UFUNCTION(BlueprintNativeEvent) 在UE中规定了需要使用对应的Interact_Implementation()函数来实现。

使用_Implementation的原因
是为了区分接口声明和实现,以及方便引擎进行处理。当声明一个BlueprintNativeEvent函数时,UE会生成两个函数:一个是用于声明的纯虚函数,比如Interact。另一个是用于实现的函数,比如Interact_Implementation
在实际调用时,UE会调用接口声明的函数,而这个函数内部会调用实现的_Implementation函数。

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

二、创建蓝图类

  1. 在UE中创建一个继承自SItemChest的蓝图类箱子,命名为TreasureChest。设置箱子的BaseLid
  2. 通过“变换”属性调整盖子的位置,使其刚好贴合在底座的上方。

三、控制箱子开关

  1. 箱子的开合效果是Pitch在改变 ,为了更方便控制打开的角度,在.h中声明了浮点型TargetPitch并使用UPROPEERTY(EditAnywhere)宏修饰(可根据需要修改),然后在.cpp构造函数中赋初值。
void ASurItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
	// 相对base进行旋转,参数(pitch, yaw, roll)
	LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
} 

四、控制动画

  1. 要实现开箱动画的控制,首先需要绑定按键事件,按下后执行某个函数,这个函数可以判断视线内一定距离内是否有箱子,有的话就将箱子打开。
  2. 根据设计模式的相关理论,开发时要尽量降低各个功能模块的耦合性,从而避免后期代码的臃肿冗余。因此在实现这个功能时,就不继续在SurCharacter类中编写具体代码,而是创建一个类来专门负责实现这部分的逻辑,然后将其与SCharacter类组合即可。
  3. 要实现这个功能,可以使用UE中的ActorComponent类。这个类可以像普通的Component一样附加到Actor上。因此派生出SInteractionComponent类,我们需要在其中实现检查周围有哪些物体可以互动,即碰撞查询collision query),所以在.h中声明PrimaryInteract()来实现这个功能需求
// SurInteractionComponent.h
class SURKEAUE_API USurInteractionComponent : public UActorComponent
{public:
	void PrimaryInteract();
}; 
  1. 总归还是要SCharacter来调用,因此要在SCharacter的两个文件中声明创建SInteractionComponent的实例,顺便再声明一下将要绑定的按键操作PrimaryInteract。也就是说本来角色执行的Interact转到互动组件那块去执行了。
  • SurCharacter.h
// SurCharacter.h
UCLASS()
class SURKEAUE_API ASurCharacter : public ACharacter
{
protected:
	UPROPERTY(VisibleAnywhere)
	USurInteractionComponent* InteractionComp;

	void PrimaryInteract();
} 
  • SurCharacter.cpp
// 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();
} 
  1. 要实现碰撞检测,游戏开发中常用发射射线的方法:从我们角色的眼镜发出一定长度的射线,当射线碰撞到第一个物体后在函数中返回这个对象。在UE中LineTraceSingleByObjectType()函数可以实现这个功能,其四个参数分别为:检测结果、射线起点、射线终点、检测参数
  2. GetOwner() 函数:用于获取当前组件所属的Actor。
  3. GetActorEyesViewPoint()函数:用于获取角色的眼睛位置和方向,常用于需要基于角色视角进行的操作。其默认实现返回Actor的位置和方向,而在ACharacter类中通常会被重载以返回头部摄像机的位置和方向。
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);
} 
  1. 碰撞检测的方法:
    (1)简单碰撞Simple Collision):使用简单的几何体(如球体、胶囊体、盒子等)来进行快速的碰撞检测,适用于不需要精确碰撞的情况。
    (2)复杂碰撞Complex Collision):使用物体的详细几何形状进行碰撞检测,适用于需要精确碰撞检测的情况,但计算开销较大。
    (3)物理引擎碰撞Physics Engine Collision):使用物理引擎(如PhysX)提供的功能进行碰撞检测,适用于物理效果较多的场景。
  2. 几种检测方法的原理:
    (1)SweepSingleByChannelSweepMultiByChannel
  • Sweep:将物体沿一条路径从起点移动到终点,并在此过程中检测是否与其他物体发生碰撞。
  • Channel:指定碰撞检测的通道,不同的通道可以有不同的碰撞响应设置。
  • 适合需要考虑物体体积的检测
  • 函数参数有:StartEnd(碰撞检测的起点和终点)、CollisionShape(定义碰撞检测的形状,如球体、盒子等)、HitResult(存储碰撞检测的结果,包括碰撞位置、法线、碰撞对象等)。
    (2)LineTraceSingleByChannelLineTraceMultiByChannel
  • Line Trace:在指定的起点和终点之间绘制一条线,并检测线段是否与任何物体相交。
  • 适用于需要高效检测的情况,如枪支射击、视线检测
    (3)OverlapSingleByChannelOverlapMultiByChannel
  • Overlap:在指定的位置创建一个碰撞形状,并检测它是否与其他物体重叠
  • Location:重叠检测的中心位置。
  • CollisionShape:定义重叠检测的形状,如球体、盒子等。
  • OverlapResults:存储重叠检测的结果,包括所有重叠的对象。
  • 可用于实现触发器:使用重叠检测实现触发区域,当物体进入或离开触发区域时触发特定事件
  1. 碰撞通道和响应:
  • 碰撞通道:定义不同类型的碰撞对象(如可见性、角色、物理体等)。
  • 碰撞响应:定义物体对不同碰撞通道的响应类型(如忽略、重叠、阻挡)。
  1. 碰撞通道的类型:
    (1)ECC_WorldStatic:用于静态物体,如墙壁、地面等。它们通常不会移动,也不参与物理模拟。
    (2)ECC_WorldDynamic:用于动态物体,如移动的角色、车辆等。这些物体可能会移动或参与物理模拟。
    (3)ECC_Pawn:用于角色(Pawn),如玩家角色或AI角色。
    (4)ECC_Visibility:用于可见性检测,如视线检测、射线检测。
    (5)ECC_Camera:用于摄像机碰撞检测,防止摄像机穿过物体。
    (6)ECC_PhysicsBody:用于物理体,参与物理模拟的物体。
    (7)ECC_Vehicle:用于车辆类物体。
  2. 碰撞响应和碰撞通道的设置:
UStaticMeshComponent* MeshComponent = ...;
MeshComponent->SetCollisionObjectType(ECC_WorldDynamic);
MeshComponent->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
  1. 最后是根据碰撞结果来调用打开箱子的函数
  2. Cast是UE中特有的类型转换函数,用于将一个基类指针(如AActor*)转换为派生类指针(如APawn*)。这是一个运行时类型检查(Runtime Type Checking)操作,可以确保转换是安全的。如果转换失败,返回nullptr

标准C++中,有多种类型转换操作,但在UE中不常用,主要原因是Cast函数具有特定的优势:
(1)类型安全:Cast会在运行时进行类型检查,确保转换是安全的。
(2)简洁性:使用Cast函数可以减少代码的复杂性,提高可读性。

  1. Execute_Interact:是一个静态函数,用于在一个实现了接口的对象上调用接口函数。它会检查对象是否实现了该接口,并调用对应的实现。

在UE中,接口函数需要通过特殊的语法来调用。接口函数通常由三个部分组成:
(1)声明的接口函数:在接口类中声明。
(2)默认实现的函数:如果存在,提供接口的默认实现。
(3)Execute_函数:用于调用接口函数。

// 从判断结果中获取检测到的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); } 
  1. 对于箱子这样有一定体积的物体使用射线检测无可厚非,但若是要实现捡硬币之类的小物品,这种方法对视角的要求就太过苛刻了。所以UE中还存在各种检测方法,如扫射、球体检测等等,开发者应该根据实际情况来选择最优的检测方法。
  • 11
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值