UE之使用C++实现关卡切换与关卡流送
实现一个离开区域范围之后切换场景的功能,效果如下:
1.设置区域的碰撞事件
首先我们创建一个Actor叫做AArea,只包含一个BoxCollision,用于模拟触发事件的区域:
BoxCollision = CreateDefaultSubobject<UBoxComponent>(TEXT("BoxCollision")); RootComponent = BoxCollision; BoxCollision->InitBoxExtent(FVector(500.0f, 500.0f, 500.0f));
这里我只希望我的角色能够触发这个区域的事件,因此还需要修改BoxCollision的碰撞响应,只保留角色的碰撞查询(或者也可以在 编辑 / 项目设置 / 碰撞 中添加自定义的碰撞通道,Object Channel一般用于物体之间的阻挡、重叠等碰撞,Trace Channel用于射线检测、扫射等查询操作的通道),同时也防止在场景切换的时候出现其他的状况:
BoxCollision->SetCollisionEnabled(ECollisionEnabled::QueryOnly); BoxCollision->SetCollisionResponseToAllChannels(ECR_Ignore); BoxCollision->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap); // 只保留角色的碰撞查询 // 也可以只启用需要的自定义Object Channels //BoxCollision->SetCollisionResponseToChannel(ECC_GameTraceChannel1, ECR_Block);
然后为这个BoxCollision添加两个UFUNCTION():
void AArea::OnBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) { if (OtherActor && (OtherActor != this) && OtherComp) { UE_LOG(LogTemp, Log, TEXT("Area Is Overlap")); } } void AArea::OnEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex) { if (OtherActor && (OtherActor != this) && OtherComp) { UE_LOG(LogTemp, Log, TEXT("Area End Overlap")); } }
注意需要在BeginPlay中绑定Overlap事件:
//BeginPlay()中 BoxCollision->OnComponentBeginOverlap.AddDynamic(this, &AArea::OnBeginOverlap); BoxCollision->OnComponentEndOverlap.AddDynamic(this, &AArea::OnEndOverlap);
在蓝图中取消勾选HiddenInGame,现在角色进入和离开区域应该会有Log出现。
2.设置委托
现在我们需要对这个事件做出响应,这里如果是蓝图我们就可以添加一个事件分发器用来广播这个碰撞事件,在C++中需要使用委托来实现:
//委托定义必须在generated.h 后 UClass前 DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAreaOverlapSignature, bool, bIsOverlapping);
这里我们在.h文件中声明一个带一个参数的委托,用来广播碰撞区域的状态,注意申明的位置一定要正确否则会报错。声明好之后就能定义委托对象了:
UPROPERTY(BlueprintAssignable) FOnAreaOverlapSignature AreaOverlapDelegate; // 委托对象定义
现在就可以使用委托对象在OnBeginOverlap()和OnEndOverlap()函数中进行广播了,将UE_LOG替换为:
// 广播委托事件,参数为true表示开始重叠 AreaOverlapDelegate.Broadcast(true); //OnBeginOverlap中 AreaOverlapDelegate.Broadcast(false); //OnEndOverlap中
现在可以在需要响应的Actor中订阅这个委托事件了,我这里就是角色类,首先在角色类的.h中添加区域的实例对象,
// 需要初始化Area实例 UPROPERTY(EditInstanceOnly) class AArea* TargetArea;
注意需要使用放置到场景中的Area对象对其进行初始化,我这里添加了一个ActorComponent用来解耦,不过不重要,过程都是一样的:
然后在BeginPlay()中订阅委托:
if(TargetArea) { TargetArea->AreaOverlapDelegate.AddDynamic(this, &ULevelStreamComponent::HandleAreaOverlap); // 订阅Area的委托事件 }
注意如果这不是最后一个场景的话,还需要重写EndPlay()函数用于取消订阅:
void ULevelStreamComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if(TargetArea) { TargetArea->AreaOverlapDelegate.RemoveDynamic(this, &ULevelStreamComponent::HandleAreaOverlap); // 取消订阅,防止内存泄漏 } Super::EndPlay(EndPlayReason); }
HandleAreaOverlap就是定义的用来处理重叠事件的函数,带有一个bool类型的参数用于接收委托参数,我们在这个函数里面再添加关卡切换的逻辑。
3.关卡切换
一般来说,关卡切换可以分为关卡加载和关卡流送,包括以下几种方式:
1.UGameplayStatics::OpenLevel()用于完全切换到一个新关卡(销毁当前关卡),如从菜单进入游戏,新关卡会作为持久性关卡存在:
// 淡出屏幕 APlayerController* PC = GetWorld()->GetFirstPlayerController(); PC->PlayerCameraManager->StartCameraFade(0.f, 1.f, 1.f, FLinearColor::Black, true, true); FTimerHandle Timer; GetWorld()->GetTimerManager().SetTimer(Timer, [this]() { UGameplayStatics::OpenLevel(this, TEXT("NewLevelName")); }, 1.f, false); // 延迟切换关卡
2.ULevelStreamingDynamic::LoadLevelInstance()用于动态加载一个子关卡(不销毁当前关卡),如加载开放世界的区域,注意该关卡可以被加载多次。加载时有卡顿(同步方式)或延迟(异步方式):
// 同步子关卡加载(主线程阻塞),可以多次加载该关卡 ULevelStreamingDynamic* LoadedLevel = ULevelStreamingDynamic::LoadLevelInstance( GetWorld(), TEXT("/Game/_project/maps/NewLevelName"), // 路径 FVector(1000, 0, 0), FRotator(0, 0, 0), bOutSuccess );
3.UGameplayStatics::LoadStreamLevel()仅加载可见部分,允许动态平滑加载,注意这种方式需要先把子关卡放到编辑器的持久关卡中,预览时会显示这个关卡,关卡的位置和旋转等需要在关卡细节中设置。卸载的时候需要手动卸载:
FLatentActionInfo LatentInfo; LatentInfo.CallbackTarget = this; //LatentInfo.ExecutionFunction = "OnLevelLoaded"; //回调函数 LatentInfo.Linkage = 0; //异步关卡流送,当前线程不会被暂停 UGameplayStatics::LoadStreamLevel(this, "NewLevelName", true, true, LatentInfo);
OK,现在回到我们的逻辑,这里采用第三种方式,首先创建一个NullLevel,里面只放一个Area,然后打开 窗口 / 关卡 然后将需要切换的关卡拖到持久关卡下:
然后实现HandleAreaOverlap中的切换逻辑:
void ULevelStreamComponent::HandleAreaOverlap(bool bIsOverlapping) { // 根据广播事件传递的参数执行不同的处理 if(bIsOverlapping) { UE_LOG(LogTemp, Log, TEXT("ULevelSystem: Overlap Event Received: TRUE")); SwitchLevel(ELevelName::DungeonLevel); //在区域内打开DungeonLevel } else { UE_LOG(LogTemp, Log, TEXT("ULevelSystem: Overlap Event Received: FALSE")); SwitchLevel(ELevelName::DefaultLevel); //不在区域内打开DefaultLevel } }
具体的实现参考上方的代码即可,注意异步加载方式回调函数会再关卡流送完成之后再调用。