Tags: InsideUE4

UE4深入学习QQ群: 456247757


引言

前文提到说一个World管理多个Level,并负责它们的加载释放。那么,问题来了,一个游戏里是只有一个World吗?

WorldContext

答案是否定的,首先World就不是只有一种类型,比如编辑器本身就也是一个World,里面显示的游戏场景也是一个World,这两个World互相协作构成了我们的编辑体验。然后点播放的时候,引擎又可以生成新的类型World来让我们测试。简单来说,UE其实是一个平行宇宙世界观。

以下是一些世界类型:

namespace EWorldType
{
enum Type
{
None, // An untyped world, in most cases this will be the vestigial worlds of streamed in sub-levels
Game, // The game world
Editor, // A world being edited in the editor
PIE, // A Play In Editor world
Preview, // A preview world for an editor tool
Inactive // An editor world that was loaded but not currently being edited in the level editor
};
}

而UE用来管理和跟踪这些World的工具就是WorldContext:



FWorldContext保存着ThisCurrentWorld来指向当前的World。而当需要从一个World切换到另一个World的时候(比如说当点击播放时,就是从Preview切换到PIE),FWorldContext就用来保存切换过程信息和目标World上下文信息。所以一般在切换的时候,比如OpenLevel,也都会需要传FWorldContext的参数。一般就来说,对于独立运行的游戏,WorldContext只有唯一个。而对于编辑器模式,则是一个WorldContext给编辑器,一个WorldContext给PIE(Play In Editor)的World。一般来说我们不需要直接操作到这个类,引擎内部已经处理好各种World的协作。

不仅如此,同时FWorldContext还保存着World里Level切换的上下文:

struct FWorldContext
{
[...]
TEnumAsByte<EWorldType::Type> WorldType; FSeamlessTravelHandler SeamlessTravelHandler; FName ContextHandle; /** URL to travel to for pending client connect */
FString TravelURL; /** TravelType for pending client connects */
uint8 TravelType; /** URL the last time we traveled */
UPROPERTY()
struct FURL LastURL; /** last server we connected to (for "reconnect" command) */
UPROPERTY()
struct FURL LastRemoteURL; }

这里的TravelURL和TravelType就是负责设定下一个Level的目标和转换过程。

// Traveling from server to server.
UENUM()
enum ETravelType
{
/** Absolute URL. */
TRAVEL_Absolute,
/** Partial (carry name, reset server). */
TRAVEL_Partial,
/** Relative URL. */
TRAVEL_Relative,
TRAVEL_MAX,
}; void UEngine::SetClientTravel( UWorld *InWorld, const TCHAR* NextURL, ETravelType InTravelType )
{
FWorldContext &Context = GetWorldContextFromWorldChecked(InWorld);
// set TravelURL. Will be processed safely on the next tick in UGameEngine::Tick().
Context.TravelURL = NextURL;
Context.TravelType = InTravelType;
[...]
}

粗略的流程是UE在OpenLevel的时候, 先设置当前World的Context上的TravelURL,然后在UEngine::TickWorldTravel的时候判断TravelURL非空来真正执行Level的切换。具体的Level切换详细流程比较复杂,目前先从大局上理解整体结构。总而言之,WorldContext既负责World之间切换的上下文,也负责Level之间切换的操作信息。

思考:为何Level的切换信息不放在World里?

因为UE有一个逻辑,一个World只有一个PersistentLevel(见上篇),而当我们OpenLevel一个PersistentLevel的时候,实际上引擎做的是先释放掉当前的World,然后再创建个新的World。所以如果我们把下一个Level的信息放在当前的World中,就不得不在释放当前World前又拷贝回来一遍了。

而LoadStreamLevel的时候,就只是在当前的World中载入对象了,所以其实就没有这个限制了。

void UGameplayStatics::LoadStreamLevel(UObject* WorldContextObject, FName LevelName,bool bMakeVisibleAfterLoad,bool bShouldBlockOnLoad,FLatentActionInfo LatentInfo)
{
if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject))
{
FLatentActionManager& LatentManager = World->GetLatentActionManager();
if (LatentManager.FindExistingAction<FStreamLevelAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)
{
FStreamLevelAction* NewAction = new FStreamLevelAction(true, LevelName, bMakeVisibleAfterLoad, bShouldBlockOnLoad, LatentInfo, World);
LatentManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, NewAction);
}
}
}

World->GetLatentActionManager()其实也算是保存在当前World里了。

思考:为何World和Level的切换要放在下一帧再执行?

首先Level的加载显然是比较慢的,需要载入Map,相应的Mesh,Material……等等。所以这个操作就必须异步化,异步的话其实就剩下两种方式,一种是先记录下来信息之后再执行;一种是命令模式立马往队列里压个命令之后再执行。注意,因为OpenLevel还要相应在主线程生成相应Actor对象,所以有些部分还是要在主线程完成的。这两种模式其实都可以达成需求,前者更加简单明了,后者相对统一。UE也是个进化过来的引擎,也并不是所有的代码都完美无缺。猜想其实也是一开始这么简单就这么做了,后来也没有特别大的改动的动力就一直这样了。引擎最终比的是生产效率的提高,确实也不是代码有多优雅。

GameInstance

那么这些WorldContexts又是保存在哪里的呢?追根溯源:



GameInstance里会保存着当前的WorldConext和其他整个游戏的信息。明白了GameInstance是比World更高的层次之后,我们也就能明白为何那些独立于Level的逻辑或数据要在GameInstance中存储了。

这一点其实也很好理解,大凡游戏引擎都会有一个Game的概念,不管是叫Application还是Director,它都是玩家能直接接触到的最根源的操作类。而UE的GameInstance因为继承于UObject,所以就拥有了动态创建的能力,所以我们可以通过指定GameInstanceClass来让UE创建使用我们自定义的GameInstance子类。所以不论是C++还是BP,我们通常会继承于GameInstance,然后在里面编写应用于整个游戏范围的逻辑。

因为经常有初学者会问到:我的Level切换了,变量数据就丟了,我应该把那些数据放在哪?再清晰直白一点,GameInstance就是你不管Level怎么切换,还是会一直存在的那个对象!

Engine

让我们继续再往上,终于得见UE大神:



此处UEngine分化出了两个子类:UGameEngine和UEditorEngine。众所周知,UE的编辑器也是UE用自己的引擎渲染出来的,采用的也是Slate那套UI框架。好处有很多,比如跨平台比较统一,UI框架可以复用一套控件库,Dogfood等等,此处不再细讲。所以本质上来说,UE的编辑器其实也是个游戏!我们是在编辑器这个游戏里面创造我们自己的另一个游戏。话虽如此,但比较编辑器和游戏还是有一定差别的,所以UE会在不同模式下根据编译环境而采用不同的具体Engine类,而在基类UEngine里通过一个WorldList保存了所有的World。

  • Standlone Game:会使用UGameEngine来创建出唯一的一个GameWorld,因为也只有一个,所以为了方便起见,就直接保存了GameInstance指针。
  • 而对于编辑器来说,EditorWorld其实只是用来预览,所以并不拥有OwningGameInstance,而PlayWorld里的OwningGameInstance才是间接保存了GameInstance.

目前来说,因为UE还不支持同时运行多个World(当前只能一个,但可以切换),所以GameInstance其实也是唯一的。提前说些题外话,虽然目前网络部分还没涉及到,但是当我们在Editor里进行MultiplePlayer的测试时,每一个Player Window里都是一个World。如果是DedicateServer模式,那DedicateServer也会是一个World。

最后实例化出来的UEngine实例用一个全局的GEngine变量来保存。至此,我们已经到了引擎的最根处:

//UnrealEngine\Engine\Source\Runtime\Engine\Private\UnrealEngine.cpp
ENGINE_API UEngine* GEngine = NULL;

GEngine可以说是一切开始的地方了。翻看引擎源码,到处也可以看见从GEngine->出来的引用。

GamePlayStatics

既然我们在引擎内部C++层次已经有了访问World操作Level的能力,那么在暴露出的蓝图系统里,UE为了我们的使用方便,也在Engine层次为我们提供了便利操作蓝图函数库。

UCLASS ()
class UGameplayStatics : public UBlueprintFunctionLibrary

我们在蓝图里见到的GetPlayerController、SpawActor和OpenLevel等都是来至于这个类的接口。这个类比较简单,相当于一个C++的静态类,只为蓝图暴露提供了一些静态方法。在想借鉴或者是查询某个功能的实现时,此处往往会是一个入口。

总结

从结构上而言,我们已经来到了最根源的地方。GEngine仿佛就是一棵大树的根,当我们拎起它的时候,也会带出整个游戏世界的各个对象。但目前这些对象:Object->Actor+Component->Level->World->WorldContext->GameInstance->Engine,确实已经足够表达UE游戏世界的各个部分。

那作为GamePlay部分而言,我们还有一个问题:UE是如何把在该对象结构上表达游戏逻辑的?

如果说:“程序=数据+算法”的话,那UE的GamePlay我们已经讨论完了数据部分,而下篇我们将开始讨论UE的游戏逻辑“算法”部分。

UE4深入学习QQ群: 456247757


个人原创,未经授权,谢绝转载!

《InsideUE4》-4-GamePlay架构(三)WorldContext,GameInstance,Engine的更多相关文章

  1. 《InsideUE4》GamePlay架构(十)总结

    世界那么大,我想去看看 引言 通过对前九篇的介绍,至此我们已经了解了UE里的游戏世界组织方式和游戏业务逻辑的控制.行百里者半九十,前述的篇章里我们的目光往往专注在于特定一个类或者对象,一方面固然可以让 ...

  2. 《InsideUE4》-10-GamePlay架构(九)GameInstance

    一人之下,万人之上 引言 上篇我们讲到了UE在World之上,继续抽象出了Player的概念,包含了本地的ULocalPlayer和网络的UNetConnection,并以此创建出了World中的Pl ...

  3. 《InsideUE4》-6-GamePlay架构(五)Controller

    <InsideUE4>-6-GamePlay架构(五)Controller Tags: InsideUE4 GamePlay 那一天 Pawn又回想起了 被Controller所支配的恐惧 ...

  4. 《InsideUE4》-8-GamePlay架构(七)GameMode和GameState

    我的世界,我做主 引言 上文我们说到在Actor层次,UE用Controller来充当APawn的逻辑控制者,也有了可以接受玩家输入的PlayerController,和能自行行动的AIControl ...

  5. 《InsideUE4》-7-GamePlay架构(六)PlayerController和AIController

    PlayerController:你不懂,伴君如伴虎啊 AIController:上来,我自己动 引言 上文我们谈到了Component-Actor-Pawn-Controller的结构,追溯了ACo ...

  6. 《InsideUE4》-5-GamePlay架构(四)Pawn

    <InsideUE4>-5-GamePlay架构(四)Pawn Tags: InsideUE4 我像是一颗棋 进退任由你决定 我不是你眼中唯一将领 却是不起眼的小兵 引言 欢迎来到Game ...

  7. 朱晔的互联网架构实践心得S1E2:屡试不爽的架构三马车

    朱晔的互联网架构实践心得S1E2:屡试不爽的架构三马车 [下载本文PDF进行阅读] 这里所说的三架马车是指微服务.消息队列和定时任务.如下图所示,这里是一个三驾马车共同驱动的一个立体的互联网项目的架构 ...

  8. 大型网站技术架构(四)--核心架构要素 开启mac上印象笔记的代码块 大型网站技术架构(三)--架构模式 JDK8 stream toMap() java.lang.IllegalStateException: Duplicate key异常解决(key重复)

    大型网站技术架构(四)--核心架构要素   作者:13GitHub:https://github.com/ZHENFENG13版权声明:本文为原创文章,未经允许不得转载.此篇已收录至<大型网站技 ...

  9. 《InsideUE4》-3-GamePlay架构(二)Level和World

    UE4深入学习QQ群: 456247757 引言 上文谈到Actor和Component的关系,UE利用Actor的概念组成一片游戏对象森林,并利用Component组装扩展Actor的能力,让世界里 ...

随机推荐

  1. 【Tip】如何让引用的dll随附的xml注释文档、pdb调试库等文件不出现在项目输出目录中

    项目输出目录(bin/debug|release)中经常是这个样子: main.exemain.pdb a.dll a.xml b.dll b.pdb b.xml ... 其中xml是同名dll的注释 ...

  2. Reflector、reflexil、De4Dot、IL指令速查表

    http://files.cnblogs.com/files/quejuwen/ReflectorInstaller.rar http://files.cnblogs.com/files/quejuw ...

  3. jquery easyui菜单树显示

    目前做了一个easyui项目需要显示多级菜单,菜单配置到数据库中,因此每级菜单都需要到数据库中取,用了jQuery EasyUI方便多了. 效果体验:http://hovertree.com/texi ...

  4. PHP 检测机器人,屏蔽内页

    PHP 检测机器人,屏蔽内页 <?php // SpiderHelper::rewrite301(); // SpiderHelper::showRobotTxt(); class Spider ...

  5. 2016 ICPC大连站---F题 Detachment

    题意:输入一个x,将x拆分成一些小的数(这些数不能相同,即x=a1+a2+......   ai!=aj when i!=j),然后这些数相乘得到一个成积(s=a1*a2*......),求最大的乘积 ...

  6. python基础之运算符

    算术运算符 运算符 描述 实例 + 加 - 两个对象相加 a + b 输出结果 31 - 减 - 得到负数或是一个数减去另一个数 a - b 输出结果 -11 * 乘 - 两个数相乘或是返回一个被重复 ...

  7. Mac上编译C++报错

    今天在使用Mac编译C++文件时,提示以下错误. Undefined symbols for architecture x86_64: "std::__1::__vector_base_co ...

  8. Java源代码-迭代器模式

    Java无疑是最成功的项目之一了,而在其中学习设计模式和架构设计,无疑是最好不过了. 概念: 提供一种方法访问容器中的各个元素,而又不暴露该对象的内部细节. 使用场景: 和容器经常在一起,我们定义了一 ...

  9. 常用C#关键字详解教程

    很多在学习网站后台的同学都对C#的关键字有些摸不到头脑,现在就和大家一起学习一下这些关键字的含义 类型 Void 用作方法的返回类型时,void 关键字指定方法不返回值. 在方法的参数列表中不允许使用 ...

  10. java语言中除数为零问题

    在以下几个例子中,输出结果如何? float aa=0; System.out.println(aa/0); System.out.println(1/aa); System.out.println( ...