一、创建箱子类和UI类
- 需要实现角色按下键盘
E
来与宝箱交互,可打开可关闭。 - 首先在UE中创建一个SurGameplayInterface类,继承自Unreal接口类,会发现.h文件中生成了两个类:USurGameplayInterface和ISurGameplayInterface。根据代码注释,第一个类不应该被修改,相关功能需要添加到第二个类中。这个类的作用是作为共享的公共接口,具体实现需要其他类来重写。
在UE中,接口实际上分为两个部分:
U接口类:是一个UObject
类,主要用于让引擎知道这个接口的存在,并管理接口的元数据。由引擎生成和管理的,修改这个部分可能会破坏引擎的正常运行。这个类通常包含静态方法和属性。
I接口类:是一个纯虚类,包含接口定义的方法。这些方法是需要在实现这个接口的类中覆盖的。
- 相关的
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);
};
- 从
AActor
和ISurGameplayInterface
(必须要继承自)派生一个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);
}
二、创建蓝图类
- 在UE中创建一个继承自
SItemChest
的蓝图类箱子,命名为TreasureChest
。设置箱子的Base
和Lid
。 - 通过“变换”属性调整盖子的位置,使其刚好贴合在底座的上方。
三、控制箱子开关
- 箱子的开合效果是
Pitch
在改变 ,为了更方便控制打开的角度,在.h中声明了浮点型TargetPitch
并使用UPROPEERTY(EditAnywhere)
宏修饰(可根据需要修改),然后在.cpp构造函数中赋初值。
void ASurItemChest::Interact_Implementation(APawn* InstigatorPawn)
{
// 相对base进行旋转,参数(pitch, yaw, roll)
LidMesh->SetRelativeRotation(FRotator(TargetPitch, 0, 0));
}
四、控制动画
- 要实现开箱动画的控制,首先需要绑定按键事件,按下后执行某个函数,这个函数可以判断视线内一定距离内是否有箱子,有的话就将箱子打开。
- 根据设计模式的相关理论,开发时要尽量降低各个功能模块的耦合性,从而避免后期代码的臃肿冗余。因此在实现这个功能时,就不继续在SurCharacter类中编写具体代码,而是创建一个类来专门负责实现这部分的逻辑,然后将其与
SCharacter类
组合即可。 - 要实现这个功能,可以使用UE中的
ActorComponent
类。这个类可以像普通的Component
一样附加到Actor
上。因此派生出SInteractionComponent
类,我们需要在其中实现检查周围有哪些物体可以互动,即碰撞查询(collision query
),所以在.h中声明PrimaryInteract()
来实现这个功能需求。
// SurInteractionComponent.h
class SURKEAUE_API USurInteractionComponent : public UActorComponent
{public:
void PrimaryInteract();
};
- 总归还是要
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();
}
- 要实现碰撞检测,游戏开发中常用发射射线的方法:从我们角色的眼镜发出一定长度的射线,当射线碰撞到第一个物体后在函数中返回这个对象。在UE中
LineTraceSingleByObjectType()
函数可以实现这个功能,其四个参数分别为:检测结果、射线起点、射线终点、检测参数。 GetOwner()
函数:用于获取当前组件所属的Actor。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)简单碰撞(Simple Collision
):使用简单的几何体(如球体、胶囊体、盒子等)来进行快速的碰撞检测,适用于不需要精确碰撞的情况。
(2)复杂碰撞(Complex Collision
):使用物体的详细几何形状进行碰撞检测,适用于需要精确碰撞检测的情况,但计算开销较大。
(3)物理引擎碰撞(Physics Engine Collision
):使用物理引擎(如PhysX
)提供的功能进行碰撞检测,适用于物理效果较多的场景。 - 几种检测方法的原理:
(1)SweepSingleByChannel
和SweepMultiByChannel
Sweep
:将物体沿一条路径从起点移动到终点,并在此过程中检测是否与其他物体发生碰撞。Channel
:指定碰撞检测的通道,不同的通道可以有不同的碰撞响应设置。- 适合需要考虑物体体积的检测
- 函数参数有:
Start
和End
(碰撞检测的起点和终点)、CollisionShape
(定义碰撞检测的形状,如球体、盒子等)、HitResult
(存储碰撞检测的结果,包括碰撞位置、法线、碰撞对象等)。
(2)LineTraceSingleByChannel
和LineTraceMultiByChannel
Line Trace
:在指定的起点和终点之间绘制一条线,并检测线段是否与任何物体相交。- 适用于需要高效检测的情况,如枪支射击、视线检测等
(3)OverlapSingleByChannel
和OverlapMultiByChannel
Overlap
:在指定的位置创建一个碰撞形状,并检测它是否与其他物体重叠。Location
:重叠检测的中心位置。CollisionShape
:定义重叠检测的形状,如球体、盒子等。OverlapResults
:存储重叠检测的结果,包括所有重叠的对象。- 可用于实现触发器:使用重叠检测实现触发区域,当物体进入或离开触发区域时触发特定事件
- 碰撞通道和响应:
- 碰撞通道:定义不同类型的碰撞对象(如可见性、角色、物理体等)。
- 碰撞响应:定义物体对不同碰撞通道的响应类型(如忽略、重叠、阻挡)。
- 碰撞通道的类型:
(1)ECC_WorldStatic
:用于静态物体,如墙壁、地面等。它们通常不会移动,也不参与物理模拟。
(2)ECC_WorldDynamic
:用于动态物体,如移动的角色、车辆等。这些物体可能会移动或参与物理模拟。
(3)ECC_Pawn
:用于角色(Pawn),如玩家角色或AI角色。
(4)ECC_Visibility
:用于可见性检测,如视线检测、射线检测。
(5)ECC_Camera
:用于摄像机碰撞检测,防止摄像机穿过物体。
(6)ECC_PhysicsBody
:用于物理体,参与物理模拟的物体。
(7)ECC_Vehicle
:用于车辆类物体。 - 碰撞响应和碰撞通道的设置:
UStaticMeshComponent* MeshComponent = ...;
MeshComponent->SetCollisionObjectType(ECC_WorldDynamic);
MeshComponent->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
- 最后是根据碰撞结果来调用打开箱子的函数
Cast
是UE中特有的类型转换函数,用于将一个基类指针(如AActor*)转换为派生类指针(如APawn*)。这是一个运行时类型检查(Runtime Type Checking)操作,可以确保转换是安全的。如果转换失败,返回nullptr
。
标准C++中,有多种类型转换操作,但在UE中不常用,主要原因是Cast函数具有特定的优势:
(1)类型安全:Cast会在运行时进行类型检查,确保转换是安全的。
(2)简洁性:使用Cast函数可以减少代码的复杂性,提高可读性。
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); }
- 对于箱子这样有一定体积的物体使用射线检测无可厚非,但若是要实现捡硬币之类的小物品,这种方法对视角的要求就太过苛刻了。所以UE中还存在各种检测方法,如扫射、球体检测等等,开发者应该根据实际情况来选择最优的检测方法。