当前位置:   article > 正文

关卡系统三、关卡切换流程_ue5 openlevel servertravel

ue5 openlevel servertravel

0 前言

在前面的文章中介绍了Level与World的概念以及它们的关系,也介绍了UE实现开放大世界的关卡流送机制,现在对Level与World以及开放大世界有了一定的了解,那么,接下来让我们思考这样一个问题,那种关卡类的游戏(即带有传送门这种)应该如何切换关卡?这里说的切换关卡是指切换PersistentLevel,根据前面的介绍,流式关卡只是在PersistentLevel(主关卡)上添加进SubLevel,并没有切换PersistentLevel,接下来让我们来看看UE中是如何实现关卡切换的。

为了避免大篇幅代码,文中涉及的代码仅包含关键部分

1 关卡切换的接口

在分析关卡切换的底层实现前,我们先来看看如果要实现关卡切换,应该怎么做。

按照一贯的套路,先会用再探究原理

创建两个Level,在其中一个Level放一个Trigger(或其他带有碰撞盒的Actor)

编辑Character的蓝图

运行游戏,当角色跑到Trigger所在的地方就会切换关卡,用法很非常简单。

2 关卡切换流程分析

前面已经会使用关卡切换的API了,那么接下来我们看看引擎中是如何实现关卡切换的。

2.1 OpenLevel接口

在分析关卡流程前,我们先来看看OpenLevel接口干了什么

  1. void UGameplayStatics::OpenLevel(const UObject* WorldContextObject, FName LevelName, bool bAbsolute, FString Options)
  2. {
  3. const ETravelType TravelType = (bAbsolute ? TRAVEL_Absolute : TRAVEL_Relative);
  4. FWorldContext &WorldContext = GEngine->GetWorldContextFromWorldChecked(World);
  5. FURL TestURL(&WorldContext.LastURL, *Cmd, TravelType);
  6. //设置将要切换的关卡信息,包括Url,TravelType
  7. GEngine->SetClientTravel( World, *Cmd, TravelType );
  8. }
  9. //设置将要切换的关卡信息
  10. void UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )
  11. {
  12. FWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);
  13. // 设置URL和Type,下一帧UGameEngine::Tick()时候会处理这些信息
  14. Context.TravelURL = NextURL;
  15. Context.TravelType = InTravelType;
  16. }

其中FURL即关卡的文件路径,有一定的格式,可以用来表示本地或者远端的关卡路径,如果是本地的关卡,那么只需要填Map字段

  1. struct ENGINE_API FURL
  2. {
  3. FString Protocol; //可以是"unreal" or "http"
  4. FString Host;//可以是"204.157.115.40" or "unreal.epicgames.com"这种
  5. int32 Port; //端口
  6. FString Map; //地图名词,比如MainCity
  7. //...
  8. };

TravelType表示绝对路径还是相对路径

  1. enum ETravelType
  2. {
  3. /** Absolute URL. */
  4. TRAVEL_Absolute,
  5. /** Partial (carry name, reset server). */
  6. TRAVEL_Partial,
  7. /** Relative URL. */
  8. TRAVEL_Relative,
  9. //...
  10. };

还有一个比较重要的类FWorldContext,它在关卡切换中扮演着非常重要的角色,它保存了关卡切换的上下文,下面将详细介绍。

2.2 关卡切换流程

从前面的OpenLevel入口可知,它仅仅是设置了TravelURL和TravelType,那么关卡到底是如何切换的?因此我们第一时间应该会想到可能是在下一次Tick的时候才真正切换的。

话不多说,直接上调用堆栈

如图所见,EngineLoop:Tick经过层层调用,最终调到了UEngine:TickWorldTravel这个函数,这就是我们要关注的重点——关卡切换的起点!至于为啥是从UUnrealEdEngine:Tick中调用是因为在PIE模式下调试的,但这不是本文的重点,感兴趣可以看看下面的代码

  1. int32 FEngineLoop::Init()
  2. {
  3. //如果不是在编辑器中的话,GEngine为UGameEngine的实例
  4. if( !GIsEditor )
  5. {
  6. EngineClass = StaticLoadClass( UGameEngine::StaticClass(), nullptr, *GameEngineClassName);
  7. GEngine = NewObject<UEngine>(GetTransientPackage(), EngineClass);
  8. }
  9. else
  10. { //UUnrealEdEngine继承自UEditorEngine
  11. EngineClass = StaticLoadClass(UUnrealEdEngine::StaticClass(), nullptr, *UnrealEdEngineClassName);
  12. GEngine = GEditor = GUnrealEd = NewObject<UUnrealEdEngine>(GetTransientPackage(), EngineClass);
  13. }
  14. return 0;
  15. }

关卡切换流程图

TickWorldTravel作为关卡切换的入口,这里会涉及到无缝切换(SeamlessTravelHandler)、服务端切换和客户端切换,其中无缝切换比较复杂,不在此处展开,而服务端和客户端的逻辑基本一致,只是传的参数不一样,所以这里就只看客户端流程。

  1. void UEngine::TickWorldTravel(FWorldContext& Context, float DeltaSeconds)
  2. {
  3. //处理无缝切换相关
  4. if (Context.SeamlessTravelHandler.IsInTransition())
  5. {
  6. Context.SeamlessTravelHandler.Tick();
  7. }
  8. //处理服务端切换关卡
  9. if( !Context.World()->NextURL.IsEmpty() )
  10. {
  11. Context.World()->NextSwitchCountdown -= DeltaSeconds;
  12. if( Context.World()->NextSwitchCountdown <= 0.f )
  13. {
  14. EBrowseReturnVal::Type Ret = Browse( Context, FURL(&Context.LastURL,*NextURL,(ETravelType)Context.World()->NextTravelType), Error );
  15. if (Ret != EBrowseReturnVal::Success )
  16. {
  17. BrowseToDefaultMap(Context); //切换到默认关卡
  18. }
  19. return;
  20. }
  21. }
  22. //处理客户端切换关卡
  23. if( !Context.TravelURL.IsEmpty() )
  24. {
  25. if (Browse( Context, FURL(&Context.LastURL,*TravelURLCopy,(ETravelType)Context.TravelType), Error ) == EBrowseReturnVal::Failure)
  26. {
  27. if (Context.World() == NULL)
  28. {
  29. BrowseToDefaultMap(Context);//切换到默认关卡
  30. }
  31. }
  32. return;
  33. }
  34. return;
  35. }

URL相关的处理

  1. EBrowseReturnVal::Type UEngine::Browse( FWorldContext& WorldContext, FURL URL, FString& Error )
  2. {
  3. WorldContext.TravelURL = TEXT("");
  4. if( URL.IsLocalInternal() )
  5. {
  6. //加载map
  7. return LoadMap( WorldContext, URL, NULL, Error ) ? EBrowseReturnVal::Success : EBrowseReturnVal::Failure;
  8. }
  9. }

开始加载map

  1. bool UEngine::LoadMap( FWorldContext& WorldContext, FURL URL, class UPendingNetGame* Pending, FString& Error )
  2. {
  3. //干掉当前World
  4. if( WorldContext.World())
  5. {
  6. //清空Player相关
  7. for(auto It = WorldContext.OwningGameInstance->GetLocalPlayerIterator(); It; ++It)
  8. {
  9. ULocalPlayer *Player = *It;
  10. WorldContext.World()->DestroyActor(Player->PlayerController->GetPawn(), true);
  11. WorldContext.World()->DestroyActor(Player->PlayerController, true);
  12. }
  13. //清空Level,AISystem、PhysicScene等
  14. WorldContext.World()->CleanupWorld();
  15. WorldContext.SetCurrentWorld(nullptr);
  16. }
  17. //开始切换到新World
  18. if (NewWorld == NULL)
  19. {
  20. //先看看内存中是否已经有这Map了
  21. WorldPackage = FindPackage(nullptr, *URL.Map);
  22. //如果没有的话,LoadPackage
  23. if (WorldPackage == nullptr)
  24. {
  25. WorldPackage = LoadPackage(nullptr, *URL.Map, (WorldContext.WorldType == EWorldType::PIE ? LOAD_PackageForPIE : LOAD_None));
  26. }
  27. //从map的packge中取World
  28. NewWorld = UWorld::FindWorldInPackage(WorldPackage);
  29. }
  30. //创建新World之后
  31. GWorld = NewWorld;
  32. WorldContext.SetCurrentWorld(NewWorld);
  33. }

这里可以看出,通过OpenLevel的方式切换关卡(PersistentLevel),实际上会将整个World切换,这里World与PersistentLevel是一一对应的。

2.3 需要注意的点

其一,在切换之前,会干掉当前世界以及其中的任何对象,因此上一个World的对象是不会保存到下一个World,只能在下一个World中重新创建!

其二,LoadPackage是在主线程中执行的(非异步线程),也就是主线程阻塞的,在切换过程中是不能做其他逻辑的,那么,我们要实现LoadingScreen这样的功能应该怎么做?

有两种解决思路:

一种是是先异步加载关卡资源,然后切换关卡的时候发现已经在内存中了(上面代码中的FindPackage),就直接切换了,但是这个有一点需要考虑,在当前关卡加载另外一个关卡,如果关卡资源很大的话,建议用一个过渡关卡(UE中叫TransistionMap)来做;

一种是UE推荐的方式,就是在切换关卡前,开启一个异步线程来做LoadingScreen相关的逻辑,大致就是用Slate框架来写(Slate是独立框架,不依赖Engine的Tick),UE提供的MoviePlayer就是用来做LoadingScreen的,具体实现不在此详述,感兴趣的可以参考官方demo ARPG工程里LoadingScreen的实现,还有一个很好用的插件也可以研究研究(AsyncLoadingScreen)。

3 总结

本文从使用OpenLevel接口开始,介绍了关卡的切换流程,用这种方式切换关卡,真正的切换时机是下一次Tick,切换关卡会导致World生成,也提到了几个注意的点和解决LoadingScreen的方案。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/501409
推荐阅读
相关标签
  

闽ICP备14008679号