【UE5转载】用Interface接口打开箱子

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

     具体的实现方式, 是使用[1]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);
} 
  1. 创建蓝图类
    在UE中创建一个SurItemChest的蓝图类箱子,命名为TreasureChest。在课程项目提供的ExampleContent文件夹中有箱子的网格体,将其分别设置给TreasureChest的Base和Lid即可。
    在这里插入图片描述
    然后通过“变换”属性调整一下盖子的位置,使其刚好贴合在底座的上方。
    调整网格体位置
  2. 控制箱子打开
    我们可以在视口中试验一下,拖拽调整盖子的角度就可以实现箱子的开合效果,调整时可以注意细节面板中“变换” -> “旋转”属性的变化,发现是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中的[2]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()函数可以实现这个功能,其四个参数分别为:检测结果、射线起点、射线终点、检测参数。关于碰撞的相关内容,可以参考[3]官方文档:

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中还存在各种检测方法,如扫射、球体检测等等,开发者应该根据实际情况来选择最优的检测方法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值