UE4学习笔记:Gameplay框架及其模块梳理(下篇)

本随笔继续上一篇随笔的内容,详细介绍Gameplay框架各个组成部分,以及这些组成部分是用哪些“数据”来驱动,以及如何驱动的。本随笔整理自Epic Games中文社区经理大钊编写的知乎专栏:InsideUE4Gameplay架构部分。

本随笔作者还在学习阶段,对UE4引擎的使用和理解还不够透彻,难免出现书写上和技术上的问题,若出现这类问题,欢迎在评论区或私信与我讨论

目录

导读

  • 每个小节的标题都以<蓝图类型名字>(<C++类型名字>)的格式命名。
  • 本随笔会在上篇的基础之上,深入上篇提到的Gameplay框架的组成部分,因此可以算是上篇的进阶内容。

Actor层面

Component(UActorComponent)

  • Component表达的是“功能”的概念,例如说赐予某个Actor根据WASD键移动、能够让某个Actor跳跃等功能,而不应该涉及“游戏业务逻辑”,例如达成某项目的之后即可让游戏通关。后者虽然也是某种功能,但是该功能是和游戏本身紧密联系的,但是前者的功能是“脱离于游戏本身”的功能。
  • Component实现的功能是需要能够直接利用到我们任何一个其他游戏上面的,也就是强解耦性。如果某个Component在移植过程中需要修改部分甚至是全部代码,那就需要考虑该Component的设计是否合理,以及是否符合“Component”强解耦性功能的定义。

Actor(AActor)

  • 如果说Component表达的是“功能”的概念,那么Actor要表达的就是“游戏业务逻辑”的概念,而UE4引擎已经使用Actor作为基类派生出了许多适合实现游戏业务逻辑的类型,如AGameMode、AGameState等类型,其中也包括了两个很重要的类型:面向对象派生下来的Pawn与Character,支持组合的Controller。

Pawn(APawn)

  • 很多时候我们需要一种类型能够响应我们的输入,特别是当我们想要控制一个对象的时候,如当我们输入的时候对象会如何移动、当我们输入的时候对象会如何格挡等,能够实现这些功能并响应我们(玩家们)的需求的类型,就是APawn类型。APawn是直接从AActor派生出来的类型。

  • APawn定义了三个基本的模块方法接口供我们使用:可被Controller控制、PhysicsCollision表示和MovementInput的基本响应接口。

    思考:为何APawn还需要被一个额外的类型Controller控制,而不是直接在APawn里面直接控制?

    • 从本质上来说用户确实可以直接在APawn里面接收用户的输入来操纵APawn,但是UE4引擎更加希望将这两种事件进行分别处理,两种事件分别是“可操控对象的表现”和“可操控对象的控制”,APawn应该更加着重于当接收到玩家输入的时候会产生什么样的“表现”,因此也就会有一个额外的类型——即AController类型——专注于如何接收并处理玩家的输入。Controller相关的类型会在本篇下文介绍。

    思考:为何Actor也可以接收玩家的输入?

    • APawn可以接收玩家输入是在情理之中的,但是实际上输入事件的功能逻辑却是在AActor里面定义的,在官方文档里面说明的“输入处理程序”也表面了游戏里面最先接收输入的对象是启用了输入事件(该条件也隐含了Actor默认不接受输入)的Actor,然后才是Controller、然后是Level Script、最后才到Pawn,而且需要注意的是,在这个输入处理顺序当中,如果某一层接收到了玩家的输入,那么之后所有的对象都将不会接收到玩家的输入,相当于是被“截取”了。
    • 对于游戏玩家来说,“输入”很多时候就是键盘输入、手柄输入等,因此在玩家可操作对象APawn上或Controller上接收输入可能更符合我们的想法,但在实际的项目当中,我们还有可能需要去实现诸如“触屏”、“声控”之类的输入功能,这些功能也归纳在输入里面,但是并不需要用到APawn作为载体,因此对于这种不确定实际输入的类型,UE4引擎干脆直接在AActor上实现输入的接口,这样无论遇到什么样的输入需求我们都能实现。

    DefaultPawn和SpectatorPawn(ADefaultPawn和ASpectatorPawn)

    • 很多时候我们会使用到APAwn,但是还需要我们自己手动一个个添加移动组件并设置控制方法,感觉很繁琐,因此UE4引擎索性直接了一个已经“组装”好基础组件的Pawn类型供用户使用,这个类型就是直接继承于APawn的ADefaultPawn类型。
    • 有时候我们在游戏里面会需要一些“观战者”角色,就是可以跳脱出常规角色的操控方法,而在场景里面随时随地“漫游”,因此UE4引擎也提供了一个特殊的类型——ASpectatorPawn类型,该类型直接继承自ADefaultPawn,并自己定义了移动的方法,让玩家能够像在空中“飞翔”一样自由漫游场景。

Character(ACharacter)

  • 可操纵角色中很多时候都会以“人形”的姿态展现,并且还会需要使用到人物动画、人物模型等类似的资源,因此UE4引擎提供了一种APawn的强化版类型,也就是ACharacter类型。
  • 该类型在APawn已经提供的功能基础之上,还提供了可供展示骨骼网格体的组件“SkeletalComponent”,接近于人形的碰撞体“CapsuleCollision”和能够模拟人类移动方式的移动组件“CharacterMovementComponent”。如果说APawn是适合于玩家操纵的类型,那么ACharacter就是适合于玩家操纵的人形类型。

Controller(AController)

  • 如上文所说,APawn负责“可操纵对象的表现”,因此我们还需要一个类型能够负责“可操纵对象的控制”,这个类型就是由AActor直接继承而来的AController类型。
  • AController定义了关联Pawn的方法:void Possess(APawn* InPawn)和void UnPossess(),用于掌控指定的Pawn,这样就可以更灵活地动态操纵某个Pawn亦或者取消操纵。
  • 因为AController继承自AActor,自身也能够接收到来自用户的输入,因此能够让用户直接在AController的里面定义该如何处理这些输入。
  • 需要注意的是,在项目里面我们也可以在任意一个Pawn上设置操纵策略,例如当这个Pawn被放置到场景里面的时候会自动生成对应的Controller并Possess,或者是当这个Pawn生成还没有被放置的时候就Possess,或者任意一种情况,或者什么都不做。

    思考:Controller和Pawn必须1:1吗?

    • 在源码里面一个Controller仅仅只能保存一个Pawn,也就是说在代码层面,UE4引擎仅支持同一时刻一个Controller仅能操控一个Pawn。
    • 但是我们可以通过在我们自己的继承自Controller的类里面保存多个Pawn来实现一个Controller对应多个Pawn的功能。
    • Controller于Pawn的关系,再次阐明了UE4引擎在设计时候的一个重要理念:对于可要可不要、可以通过其他方式实现的功能,UE4引擎会更加倾向于舍弃这些功能的实现,而让这些功能交由开发者来实现。

    思考:Controller的位置有什么意义?

    • 默认的Controller类型是自带一个SceneComponent组件的,这也就说明了Controller本身自带位置信息,但是Controller仅仅是用来处理输入逻辑的,那为什么还需要一个位置信息?其实带入到第三人称游戏里面就很好理解了,有的时候我们需要我们玩家的视角在上下左右四个方位上查看周围场景,但是我们又想在查看过程中不去让玩家控制的角色跟随着转动,也就是说Controller自带位置的意义在于将”玩家的位置”和“角色的位置”区别开来。

    思考:哪些逻辑应该写在Controller当中?

    • 正如上文所说,AController着重表达“可操控对象的控制”的概念,而APawn着重表达“可操纵对象的表现”的概念,因此在AController当中最需要处理的就是“当我们接收到玩家输入后要怎样处理”的逻辑。
    • 如果是可以抽象为同一类型的逻辑操作,例如操纵对象的前后左右移动,那么我们可以将该逻辑写进AController里;如果是某些特定于固定类型,例如某一类型操纵对象可以开炮,那么开炮逻辑我们就可以编写到APawn里面而不是AController里面。
    • 从对象生命周期上来说,AController比APawn的周期要长,在游戏过程中会遇到操纵对象死亡的情况,一般来说操纵对象死亡之后就会被Destroy掉,然后重新Spawn一个新的对象,这时对象身上的属性都会被重置掉,因此我们在编写一些不需要被重置的功能时就可以将其写进AController里面。

    PlayerState(APlayerState)

    • AController本身可以通过定义各种各样的变量来实现存储玩家信息的功能,但是UE4引擎更加细致的提供了一个独立去AController的类型供用户来实现存储玩家信息的功能,这个类型就是APlayerState。
    • 使用APlayerState类型就可以根据需要来定义变量实现存储功能,而且相较于直接存储在AController里面的方式,存储在APlayerState里面可以让玩家信息数据于AController“分离”,因为AController类型里面仅仅只是存储了一个指向APlayerState类型的指针,实际上APLayerState是生成在Level里面的,也就是说是和APawn、AController是平级的,这样无论是重新生成APawn还是AController,APlayerState里面的数据也都会一直存在。
    • 将PlayerState和PlayerController分开还有一个好处,那就是在网络游戏中,如果玩家因为网络波动问题掉线了,其Controller和Pawn都会被释放掉,这时可以让服务器暂存PlayerState,等玩家上线之后再挂载到玩家身上,就可以实现一个无缝地联机体验。
    • 虽然官方在代码里面注释说只有玩家需要PlayerState,而NPC不需要,但实际上存储APlayerState的变量是定义在了AController类型里面,而不是其直接派生类APlayerController或AAIController(这两个类将在本随笔的下一节进行讲解),因此也说明了做为NPC的AI也是可以拥有PlayerState的。这也很好理解,如果能让NPC读取到玩家的PlayerState然后据此做出一些决策的话,也能够成为游戏机制的一环的。

      思考:哪些数据应该放在PlayerState中?

    • 从类型名字上就可以得知,该类型应该存储的是玩家的信息,诸如玩家的得分情况,因此像是游戏的信息(竞技游戏中当前对局的比分等)就不应该放到PlayerState里面,而应该放到GameState里面(GameState类型将会在本随笔下文部分介绍)。
    • 需要注意的是因为PlayerState实际上是生成在关卡里面的,切换关卡的时候PlayerState会被释放的,所以PlayerState实际上代表的是“当前关卡中玩家的信息”,如果是需要横跨关卡、代表整个游戏过程的数据,那最好是存储到GameInstance里面。

PlayerController(APlayerController)

  • 在大部分游戏中,角色对象可分为两类:一类是玩家可直接操纵的角色,另一类是玩家不可操纵,而是由AI来操纵的角色。UE4引擎提供了从AController直接继承而来的类型APlayerController来代表其操控者为玩家。
  • 相较于其直接基类AController,APlayerController显然考虑到了玩家们的需要,因此添加了摄像机管理、存储当前玩家(UPlayer)等功能。

    思考:哪些逻辑应该放在PlayerController中?

    • PlayerController最适合编写那些实现由玩家直接操控的对象所需要的输入逻辑,例如接收通过键盘或者是手柄产生的输入并进行正确的逻辑判断(如要不要响应输入,什么时候该响应什么时候不该响应等)。

AIController(AAIController)

  • 在上一小节提到角色对象可分为两类,其中一类就是不由玩家操控、而是由AI操控的角色。可实现AI操控功能的类型就是从AController直接继承而来的类型AAIController。
  • 相较于直接基类AController,AAIController并不需要APlayerController那样的摄像机、存储当前玩家等功能,相反,该类型添加了能够支持AI系统的AI组件:运行行为树、使用黑板数据、探索周围环境等。

    思考:哪些逻辑应该放在AIController里面?

    • 就像Controller控制Pawn一样,AIController也应该编写一些功能逻辑以操控其控制的Pawn或Character。
    • 从实现上来说,我们可以直接在AIController里面实现硬编码,从而实现“AI逻辑”,但是最推荐的方法是Epic Games实现的行为树系统及黑板组件。

Level层面

GameMode(AGameMode)

  • 我们已经知道,无论是UWorld还是ULevel,其主要职责还是将所有需要使用到的Actor包含进去,也就是对象的“显示”,但是一个游戏只有显示是不够的,还需要有一个“游戏规则”才能够让一场游戏进行下去,这个负责指定游戏规则并“控制”整个World以及所有Level的类型,就是AGameMode类型。
  • GameMode登记了一些例如Pawn、PlayerController、PlayerState等等的基础类型,这样让开发者在设计游戏规则的时候能够更加便捷,。
  • 为了控制整个游戏的执行进度,诸如SetPause、RestartPlayer这类方法也都是实现在了GameMode对象里面。
  • GameMode也控制了World的切换,甚至当我们启用了bUseSeamlessTravel之后,可以重载GameMode和PlayerController里面的GetSeamlessTravelActorList方法来指定哪些Actor不会被释放而是直接被“传送”到下一个World。
  • GameMode甚至可以控制网络游戏中每个玩家的加入情况。
  • 在联网游戏当中,GameMode仅仅只会存在于服务器上。

    思考:配置不同GameMode的多个Level组成的World采用的是哪一个World的WorldSettings?

    • 一个World里面只会生成一个GameMode,否则整个游戏肯定会乱套了。
    • UE4引擎仅仅只会在加载World时,加载那个World的Persistent Level使用的GameMode,哪怕是后续再加载流送子关卡,整个World也仅仅只会有Persistent Level使用的那个GameMode。

    思考:当我们切换World(也就是切换Persistent Level)时,GameMode是否保持一致?

    • 只要是切换了World,那么当前World里面所有已经生成的对象都会被释放掉(包括GameMode、GameState等),然后再重新生成新World里面的GameMode,也就是说GameMode并不会保持一致。
    • (随笔作者注:GameMode里面有个属性bUseSeamlessTravel,可以实现World部分对象的传递,但是其中并不包括GameMode对象。关于bUseSeamlessTravel属性还需要后续实践来了解)

GameState(AGameState)

  • 我们已经知道APlayerState是用来存储每个玩家的信息状态的,那么对于一个游戏,我们也可以有一个类型用来保存游戏的信息(比如说各个队伍获得的总分数等),这个类型就是AGameState。AGameState同APlayerState、AWorldSettings一样都是直接继承于AInfo。
  • AGameState类型里面的成员变量FName MatchState和相关的回调时用于在网络中传播传播同步游戏状态的,因为GameState在网络游戏中是存在于服务器上且能被复制到客户端的。
  • AGameState还保存了所有玩家的APlayerState。

GameSession(AGameSession)

  • 该类型主要是用来实现网络联机游戏当中的Session,本文重点不在于网络,对于该网络相关类型不做过多介绍,因此暂时忽略。

AGameModeBase和AGameStateBase

  • 原版的AGameMode类型和AGameState类型内置了许多网络联机游戏才合适的接口,例如匹配状态、玩家总人数等,但是这些信息都只是网络联机游戏才需要的,为了能够更加适应其他游戏类型,UE4将AGameMode和AGameState里面更加抽象的一些逻辑抽离了出来,分别生成了新的类型——AGameModeBase和AGameStateBase,如果我们在实现我们自己的Game Mode和Game State时候不需要很复杂的逻辑,那么我们就可以选择这两种类型的“Base”版本来作为我们自定义类型的基类。

Player层面(处于Actor层和Game层之间的一个中间层)

  • 无论是Actor层面的对象还是Level层面的对象,我们都是在搭建一个可供游玩的环境,当我们的可游玩环境设计好了之后,自然是需要我们玩家自身加入进去,这里的“玩家”并不是指玩家控制的APawn或ACharacter,也不是指APlayerController,而是指实实在在的、我们玩家自身在这个Gameplay框架里面的代表

Player(UPlayer)

  • 指代实际玩家对象的类型就是由UObject直接继承而来的UPlayer,之所以不继承自AActor是因为Actor及其派生类都必须被放置到场景里面才能够正常使用,而且在切换Level(即切换World)的时候其内的所有Actor都会被释放,但实际上Player类型应该是独立于任何Level,并且长期存在的对象,因此最适合继承自UObject。
  • UPlayer类型定义了一个APlayerController* PC成员变量,也提供了设置该变量的接口,这也就是说引擎会为每个Player都设置对应的输入响应,这也很符合“玩家”的概念。

LocalPlayer(ULocalPlayer)

  • 顾及到UE4引擎会被使用在到网络联机游戏上,因此引擎也对玩家进行了一个分类:分为“本地玩家”和“远程玩家”。其中用来表示本地玩家的类型就是从UPlayer直接继承而来的ULocalPlayer。
  • ULocalPlayer相比其基类UPlayer多了Viewport相关的功能逻辑。作为本地玩家肯定需要将我们的输入反馈到本地的输出设备上(例如屏幕)。
  • 因为在Player层面上面就到了Game层面,因此在UGameInstance类型里面会包含一个统计了所有本地玩家的数组TArray<ULocalPlayer*> LocalPlayers。
  • UE4内部创建玩家的过程可以表示为:UE4在创建GameInstance的时候,会先默认创建出一个GameViewportClient,然后在内部再转发到GameInstance的CreateLocalPlayer创建出LocalPlayer之后再创建相应的PlayerController,在PlayerController生成过程中会在内部调用InitPlayerState,至此就将Player和PlayerState全都联系起来了。

    思考:为何不在ULocalPlayer里面编写逻辑?

    • 虽然Player类型代表了很重要的每个玩家对象,但实际上UE4引擎并没有直接的LocalPlayer蓝图类,甚至官方文档里面也没有详细描述如何去创建自己的ULocalPlayer类。原因很可能是每个玩家“操纵”的对象已经根据功能性解耦成了很多独立的模块:用以表示“玩家表现”的APawn和ACharacter,用以表现“玩家操控”的AController和APlayerController,以及用于“接收玩家输入”的UPlayerInput等,因此在表现一个“玩家”的对象上————也就是ULocalPlayer和UPlayer————也就不需要再去添加额外的功能逻辑,而是通过这些独立的模块来完成“玩家”的功能逻辑。
    • 实际上UE4引擎还是提供了使用玩家自定义ULocalPlayer的设置,不过只能够创建C++类而不能创建蓝图类,当我们自己的LocalPlayer类编写好了之后只需要在UE4编辑器主界面上的“编辑(Edit)->项目设置(Project Settings)->引擎(Engine)->常规设置(General Settings)->默认类型(Default Class)->本地玩家类型(Local Player Class)”里选择我们自己的自定义类,这样就能让引擎在生成LocalPlayer的时候创建我们自定义的类型。

NetConnection(UNetConnection)

  • 如上文所说,在网络联机游戏上玩家可以分为“本地玩家”和“远程玩家”,其中ULocalPlayer代表了本地玩家,那么代表远程玩家的就是本小节将要介绍的同样直接继承自UPlayer类型的UNetConnection类型。
  • “远程玩家”仅仅只是相对于“本地玩家”而言的,因为远程玩家实质上并不存在于本地设备上,而是通过将服务器上的信息复制到本地上来进行展示的,因此该类型相较于ULocalPlayer类型,少了需要输出相关的逻辑。

Game层(仅次于Engine层)

GameInstance(UGameInstance)

  • 我们已经知道在整个引擎的Engine层里是可以允许多个GameInstance存在的,除游戏本身的GameInstance以外,编辑器的、PIE的都能算独立的GameInstance,因此这很有可能也是这种类型被命名为“Game Instance(游戏实例)”而不是单纯的“Game”的原因。而GameInstance都是由Engine层(UGameEngine和UEditorEngine)进行管理,这就将“Game”的概念和“Engine”的概念区分开了。

    思考:哪些逻辑应该写在GameInstance?

    • Game层的GameInstance在Gameplay关系树里面是高于ULevel和UWorld的一个层,因此GameInstance非常适合用于实现切换Level相关的逻辑。
    • 虽然UE4引擎已经默认实现了创建Player的逻辑,但如果我们想要一些实现自定义生成Player的功能逻辑的话,GameInstance可以提供很多常用的接口。
    • UE4引擎的UI系统是一套独立于World的系统,并不像APlayerController、APawn、AGameMode等类型会生成在关卡当中,也就是说需要将UI相关的逻辑实现在Level层之上,GameInstance也因此非常适合用来实现UI相关的逻辑。 (随笔作者注:从我个人的开发经验上来说,我们使用UMG来编写UI,然后会直接在具体的Actor里面去管理UI的生成和销毁以及部分接口的调用,但很多时候会发现很多不适合于项目需求的地方,大钊这部分的文章给了我一个新的使用UI的方式,在以后的开发过程中可以结合着使用试试。)
    • 适用于全局配置的读取(例如游戏画质设置、操控设置等)。
    • 就像PlayerState是用于存储单个玩家信息,GameState用于存储单个世界(UWorld)的信息,如果我们需要存储一些生命周期大于UWorld和ULevel的信息,那我们可以选择直接在GameInstance里面创建变量。但是需要注意的是,GameInstance的生命周期也仅仅只是游戏启动一直到游戏关闭的这段“临时”周期,如果我们还需要数据能够存储到本地,供我们下一次启动时候读取从而实现类似“游戏存档”功能的话,最好使用SaveGame。

番外:SaveGame(USaveGame)

  • 该类型并不属于本随笔上述的Actor层、Level层或Game层,而是一个直接从UObject类直接继承而来的类型。
  • USaveGame本质上就只是一个提供给UE4用来序列化和反序列化的对象,因此本身并不需要任何控制,我们只需要在其类型内定义我们需要保存的数据即可。
  • 实际控制USaveGame的序列化和反序列化操作的是蓝图函数库GameplayStatics提供的接口:CreateSaveGameObject、LoadGameFromSlot等。

番外:Subsystem(USubsystem)

  • Subsystem(子系统)是一套(而不是一个)可以定义自动实例化和自动释放的框架,说白了就是UE4引擎能够在合适的时候生成实例,也会在合适的时候释放实例的对象。
  • 该对象直接继承自UObject,因此其类型名字就为USubsystem。但当我们在使用子系统的时候我们并不会直接继承自USubsystem,而是继承自它的五大子类:UEngineSubsystem、UEditroSubsystem、UGameInstanceSubsystem、ULocalPlayerSubsystem和UWorldSubsystem。
  • 子系统的最方便的地方在于其“自动托管”的生命周期,也就是说五大子系统的所有派生类都能够在特定的场合里生成实例,也能够在特定的场合里销毁这些实例,当我们掌握五大子系统的不同生命周期之后,可以任意选择适合我们的子系统作为直接基类来派生我们自己定义的子系统类。
  • Subsystem的创建和使用于通常的UObject派生类一致,只需要在创建C++类的时候,选择一个子系统作为其基类,然后再在类体里面定义我们需要的变量和方法即可。就算是蓝图反射机制(如“BlueprintCallable”)也完全可以使用到我们自定义的子系统上————子系统本质上就只是一个UObject!

    子系统加载的流程

    • 子系统在UE4引擎里面的生成流程可以大致归结如下:
      1. UE4引擎在创建了UGameInstanceSubsystem(其他类型的子系统也是)之后,会创建所有继承自该类型的派生类的所有对象,需要注意的是这些对象都是单例,也就是说每种类型只会生成一个对象,类似于“全局变量”。
      2. UGameInstanceSubsystem初始化时,将在其子类的子系统上调用Initialize()
      3. UGameInstanceSubsystem关闭时,将在其子类的子系统上调用Deinitialize()
      4. 此时将放弃对子系统的引用,如果不再有子系统被引用,则其将被垃圾回收。

    子系统的优点

    • 使用子系统而不是我们自己去创建一个管理类(继承自UGameInstance等等)会为我们带来非常多的便利:
      1. 更方便的管理。不需要我们去手写我们自定义的单例模式管理类,不需要去判断单例,因此会降低我们需要编写的代码量。
      2. 模块化。普通的UGameInstance类,我们在编写逻辑的时候很有可能全部都会塞进这个类型里面,但是子系统允许我们继承自UGameInstanceSubsystem生成很多个自定义子系统,每个子系统都会被生成,因此我们可以很方便将原本臃肿的逻辑给模块化,使代码看起来更加简洁。
      3. 生命周期更加细腻。在五大子系统类型上,不同的子系统类型有着不同的生命周期,需要选择适合我们需求的子系统作为基类派生出我们自定义的类型,远比笼统的使用UGameInstance派生的类型使用一致、但有时候不是必须的生命周期要来得便利。
      4. 上下文菜单访问。虽然子系统看起来和“全局变量”、“全局函数”很相似,但是和这类可以在任意位置调用的变量、函数不同的是,我们在获取对应子系统的时候,获取函数会自动检查当前对象里能否调用到我们的子系统,从而返回实际的子系统对象或空指针。例如我有一个UWorldSubsystem子系统,但是我却在UObject或是UUserWidget里面尝试获取该子系统,即使子系统是确实存在的,但我们实际上只能够获得一个空指针。

    子系统的生命周期

    • 五类子系统的生命周期和其五类Outer对象的生命周期是几乎一致的:
      1. UEngineSubsystem的Outer对象是UEngine GEngine*,代表引擎。无论是Editor模式还是Runtime模式全局范围内只会存在一个该对象。从进程启动开始时候创建,到进程结束时候销毁。
      2. UEditorSubsystem的Outer对象是UEditorEngine GEditor*,代表编辑器。在Editor模式下只会存在一个。从编辑器开始启动开始创建,到编辑器结束销毁。
      3. UGameInstanceSubsystem的Outer对象是UGameInstance GameInstance,代表一场游戏,无论是Editor模式还是Runtime模式都只会存在一个。从游戏启动开始创建,到游戏结束销毁。需要注意的是*Editor模式下只有在点击Play按钮,也就是PIE模式下才会创建,因此也会在点击Stop按钮,也就是结束PIE模式的时候销毁,在这个过程中UGameInstanceSubsystem会不断生成销毁,因为其Outer对象UGameInstance* GameInstance也是在不断生成销毁的。
      4. ULocalPlayerSubsystem的Outer对象是ULocalPlayer LocalPlayer,代表本地玩家,其数量会根据玩家的数量不同而不同,单人游戏中就只会存在一个。会随着第一个LocalPlayer的创建而创建,也会随着最后一个LocalPlayer的销毁而销毁*。
      5. UWorldSubsystem的Outer对象是UWorld World*,代表一个世界。在Runtime模式下只会存在一个,但是会随着关卡的切换(即Persistent Level的切换)而生成销毁,在Editor模式下会存在多个,因为不仅仅是PIE算一个World,编辑器视口里,甚至编辑器本身也会算作是一个World!
      • 总结来说,就是每个子系统都会随着其第一个Outer对象的创建而创建,也会随着最后一个其Outer对象的销毁和销毁。
      • 五类子系统中有两类子系统是属于UDynamicalSubsystem的:UEngineSubsystem和UEditorSubsytem。之所以被称为“动态子系统”是因为这两类子系统及其派生类都是根据其所在模块的加载而创建,随着模块的卸载而销毁

    如何获取到子系统对象

    • 在我们定义好我们自己的子系统类之后,其生成和销毁完全是有引擎控制的,我们只需要通过一些方式即可获取到子系统对象:
      1. UEngineSubsystem
      UMyEngineSubsystem* MySubsystem = GEngine->GetEngineSubsystem<UMyEngineSubsystem>();
      
      1. UEditorSubsystem
      UMyEditorSubsystem* MySubsystem = GEditor->GetEditorSubsystem<UMyEditorSubsystem>();
      
      1. UGameInstanceSubsystem
      UGameInstance* GameInstance = ...;
      UMyGameSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameSubsystem>();
      
      1. ULocalPlayer
      ULocalPlayer* LocalPlayer = ...;
      UMyPlayerSubsystem * MySubsystem = LocalPlayer->GetSubsystem<UMyPlayerSubsystem>();
      
      1. UWorldSubsystem
      UWorld* World = ...; 
      UMyWorldSubsystem* MySubsystem=World->GetSubsystem<UMyWorldSubsystem>();
      

    思考:如何用好子系统?

    • Subsystem就是Gameplay级别的Component。就像是Actor可以通过Component来扩展功能一样,Gameplay框架也可以通过Subsystem来扩展功能,并依赖Gameplay框架里面的对象。如果我们需要实现一系列依赖于Gameplay里面的对象(例如依赖UGameInstance,但是又想扩展UGameInstance的功能),就可以选择在Subsystem里面添加逻辑。
    • USubsystem只是一个普通的UObject。一切我们可以在UObject上进行的操作,例如定义变量、定义函数、暴露给蓝图等等的操作,我们在USubsystem里面也同样可以实现。
    • 取代Manager类。我们自己编写Manager类的话,需要很小心的去管理Manager类的生命周期,而USubsystem类已经帮我们掌控好了生命周期,因此我们可以更专心地去编写游戏逻辑。
posted @ 2022-07-14 15:32  U_N_Owen  阅读(1522)  评论(0编辑  收藏  举报