-
创建箱子和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);
}
- 创建蓝图类
在UE中创建一个SurItemChest的蓝图类箱子,命名为TreasureChest。在课程项目提供的ExampleContent文件夹中有箱子的网格体,将其分别设置给TreasureChest的Base和Lid即可。
然后通过“变换”属性调整一下盖子的位置,使其刚好贴合在底座的上方。
- 控制箱子打开
我们可以在视口中试验一下,拖拽调整盖子的角度就可以实现箱子的开合效果,调整时可以注意细节面板中“变换” -> “旋转”属性的变化,发现是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中还存在各种检测方法,如扫射、球体检测等等,开发者应该根据实际情况来选择最优的检测方法。