那一天
Pawn又回想起了
被Controller所支配的恐惧html
如上文所述,UE从Actor中分化了一些专门可供玩家“控制”的Pawn,那咱们这篇就专门来谈谈该怎么个控制法!
所谓的控制,本质指的就是咱们游戏的业务逻辑。好比说玩家按A键,角色自动找一个最近的敌人并攻击,这个自动寻找目标并攻击的逻辑过程,就是咱们所谈的控制。
Note1:重申一下,Controller特别是PlayerController,跟网络,AI和Input的关系都很是的紧密,目前都暂且不讨论,留待各自模块章节再叙述。程序员
无论是游戏,仍是其余App,Web或Server等,本质上都是程序,因此也都是或多或少须要一些程序逻辑。从1843年拜伦的女儿Ada Lovelace用穿孔机编写第一个程序开始,到2016的今天咱们能方便地用蓝图连线组织程序逻辑,应该归功于一代代软件工程师们孜孜不倦的探索。时代在发展,技术在进步,软件也愈趋于复杂多变,不少软件的庞大也已经超越了我的的理解容量极限(UE?),所以咱们就愈来愈须要设计方法来让咱们可管理庞大的复杂度。几十年的迭代,旧的模型被放弃,新的模型被提出验证,工程师们在这过程当中总结积累出了一些设计模式。最负有盛名的应该是GOF的《设计模式》,以及MVC,MVP,MVVM等。本文的重点不在于细谈论各类设计模式,若是有对设计模式不清楚的读者,请务必仔细去研究学习,因UE如此庞大的代码框架也是充斥着各类设计模式的应用,设计模式理解得越好,也越能理解UE的框架设计。
言归正传,设计模式的本质就是抽象变化。若是依照纯朴的"程序=数据+算法"的结构来看,再算上用于用户显示和输入的界面,那么就获得“程序=数据+算法+显示”。这三大基本块(数据,算法,显示)构成了程序的三大变化,而如何把这三者“+”到一块儿,用的就是咱们的种种设计框架模式。
典型的,对于游戏:算法
抽象这三个变化,并概括关系,就是典型的MVC模式了:
有些人可能会说MVC是UI专用的模式,如IOS的MVC或WPF的MVVM,也或者说由于游戏的类型千差万别因此一个通用的框架并不能都适用,所以就有一点点想要“返璞归真”的意味,以为游戏引擎只须要提供一个基本的渲染框架,其余的逻辑框架不须要设计复杂,开发者们可自行根据游戏类型再设计。这种观点有必定的道理,对于简单的游戏或Demo,确实也还不到须要“设计”的地步;而对于复杂大型的游戏,须要的架构知识也确实远不是MVC这么简单。但缺点在于,说这话的人要嘛就已是架构高手,各类设计模式信手拈来,早已经到了无招胜有招的地步;要嘛就是回避了问题,游戏也是软件,软件的固有复杂度摆在那里,总得须要个办法去解决,今天若是咱们不是在探讨尝试用MVC模式去掌控它,也是在谈一个别的名字的模式。在我看来,一个好的游戏引擎,应该是能尽力的帮助用户,并减小麻烦。MV固然也有它的缺陷和不足,因此咱们应该研究的是UE为什么选择了MVC,有什么优势缺点,咱们怎么利用和规避,让UE的Controller们尽责的为咱们服务,少形成麻烦。
对于简单的游戏或者引擎来讲,有时并不须要把这三者分的很清,如Cocos2dx就没有Controller,它的MVC就是混杂在一块儿,由于代码量少因此也还算勉强能凑合;Unity的MonoBehavior其实也至关于把MC放在了一块儿,用得方便的同时也得当心太顺手了出现组件之间互相网状引用一团乱麻的状况;UE在这个问题上的思考就有些一脉相承,既然Actor们形形色色,咱们以前也谈过甚至有AInfo这种书记官,那为什么不让一些Actor专门来承载逻辑呢?因而,Actor再度分化出Controller。下面咱们就来一一介绍Actor旗下Controller家族的指挥官们。设计模式
虽然我在以前已经一再的剧透过AController是继承自AActor的一个子类,可是为了更好理解思考UE里的Controller机制,请先把脑壳放空,也别去偷看UE里的源码,像张无忌同样暂时忘记AController这回事,问本身一个问题:若是我想实现一种机制去控制游戏里的Actor,该怎么设计?
巧妇难为无米之炊,我们先来看看当前手上都有些什么:数组
针对APawn,再想一想咱们但愿达成的控制愿景,没事,你尽管放开想象的想,作不作获得我们先放一边,但至少别在一开始就被想象力限制住了。“控制”自己虽然只是一段逻辑算法代码,可是它也须要有个载体去承载和运行,某种意义上来讲也算得上是个实体。因此下面咱们不妨就脑洞大开,以“控制”这个实体的视角口吻,讲讲“我,做为一个——控制”但愿拥有哪一些本领:服务器
在仔细考察了"控制"的需求和手头上的原料以后,咱们试着从UE的角度来权衡一下。
首先Controller不能是一个Component,一是由于Component的层级过低,表达的是功能的概念而非逻辑;二是Component必须依附于Actor存在,而咱们的Controller但愿能独立存在。
其次若是从UObject直接继承下来UController,却是也可行,UObject也能复制同步,其余的控制Pawn的能力和事件响应等倒也是能改改接口想一想办法,可是要想在世界里移动,就得有个位置表示,再加上还但愿能容纳Components,这就麻烦了,基本就是把Actor的工做再作一套,有点累人,搞起来也怕两套班子出错闹矛盾。
再来考察下从AActor继承下来AController怎么样,Actor比Object多了一些咱们正须要的配置动态生成、输入事件响应、Tick、可继承、可容纳Component、可在世界里出现、可在网络间同步。好了,如今就差控制Pawn的能力,那咱们就在这个分化出来的AController增长一些控制Pawn的接口,这个思路正是和咱们从Actor从分化出Pawn的时候不谋而合!如今咱们来看看UE里的AController:
跟咱们的设计八九不离十,咱们再一一仔细验证一番:
关联Pawn的能力,有Possess和UnPossess,源码里也有PawnPendingDestroy等这些函数(未一一列出);GameMode中也保存着AIControllerClass和PlayerControllerClass的配置,用于在适当的时候Spanw出Controller;继承于Actor也就有了EnableInput和Tick;Controller自己还能够继续派生下去(如AIController和PlayerController),也能够容纳Components;也带着一个SceneComponent因此能够摆放在世界中;自身也能够添加成员变量来记忆存储游戏状态;自身也有一个FName StateName(Playing、Spectating、Inactive),切换自身的状态(运行,观察,非激活);由于跟Pawn是平级的关系,只在运行的时候引用关联,因此对彼此独立存在不作强制约束,提升了灵活性。一个Pawn自身上也能够配置策略:网络
namespace EAutoReceiveInput { enum Type { Disabled, Player0, Player1, Player2, Player3, Player4, Player5, Player6, Player7, }; } TEnumAsByte<EAutoReceiveInput::Type> AutoPossessPlayer; enum class EAutoPossessAI : uint8 { /** Feature is disabled (do not automatically possess AI). */ Disabled, /** Only possess by an AI Controller if Pawn is placed in the world. */ PlacedInWorld, /** Only possess by an AI Controller if Pawn is spawned after the world has loaded. */ Spawned, /** Pawn is automatically possessed by an AI Controller whenever it is created. */ PlacedInWorldOrSpawned, }; EAutoPossessAI AutoPossessAI; TSubclassOf<AController> AIControllerClass;
这样在运行时UE也能够根据Pawn建立配套的Controller。毕竟只是为了阐明概念,而不是纠结技术细节,我对Controller的功能接口都只是粗略带过,若是读者本身去看Contoller的UE源码,顺即可以对我当前说的概念验证一下,还会发现一些Movement和ViewPoint的接口,这些也算是和控制移动和视角配套吧。架构
思考:Controller和Pawn必须1:1吗?
观察UE实现里咱们发现Controller里只是保存了一个Pawn指针,而不是数组,这和一开始但愿的多对多关系有些出入。理想和现实老是有差距,一个愿景落实到工程实践上也难免得有一些妥协。首先咱们再来梳理理解一下这个Possess(拥有/占用)的概念。一个Controller能灵活的Possess/UnPossess一个Pawn,虽然一次只能控制一个,但在游戏中咱们也能够在不一样的Pawn中切换,好比操纵一个主角坐进而后控制一辆汽车,或者端起固定的机关枪扫射,这些功能琢磨一下其实只是涉及操做实体Pawn的变化。若是咱们能妥善的用好Pawn和Controller的切换功能,大部分基本的游戏功能也是可以比较方便的实现的。那么有哪些是不太适合的呢?UE官方其实也认可了,见Controller文档说明:mvc
By default, there is a one-to-one relationship between Controllers and Pawns; meaning, each Controller controls only one Pawn at any given time. This is acceptable for most types of games, but may need to be adjusted as certain types of games - real-time strategy comes to mind - may require the ability to control multiple entities at once.框架
对于RTS这种须要一会儿控制多个单位的游戏来讲,这种1v1的关系确实比较僵硬,就须要在Controller里本身实现扩展一下,额外保存多个Pawn,而后本身实现一些须要的控制实现,但整体上也只能说得绕一下,也算不上特别复杂,因此就也不能说UE作不了某一些类型的游戏,Epic是个游戏引擎公司,卖的毕竟是个通用游戏引擎。
OK,那UE为什么不实现成多对多呢?我以为理由每每很简单,就是想保持必定的简单。游戏引擎的每一个模块的设计,甚至函数接口的设计,无时无刻不在权衡决定。太简单了概念清晰用起来方便可是灵活扩展力不足,太灵活扩展无限了每每也会让人无从适从容易出错。当前1:1的时候,咱们的脑壳逻辑很清晰,咱们能够在Controller里直接GetPawn,也能够在Pawn中GetController,都很是方便。调试逻辑Bug的时候,咱们也能很快找到查错的目标。而对比想象,若是是M:N,灵活性是满满了,可是你能轻易的说出当前Pawn是被哪些Controller控制吗?你也得时时记着这个Controller当前控制了哪些Pawn。OMG!这些Pawn和Controller多对多的构成了网状结构,项目越庞大复杂,这张网也越能套住你。再从另外一个方面说,一旦提供了这种多对多的直接支持,以咱们人类的性格,免费现成的东西,咱们老是倾向于去找机会能用上它,而不是去琢磨到底应不该该用。因此一旦就这么直接提供了,对于刚入门的新手,压根就没什么指引,怎么来好像均可以,就很是容易收不住把项目逻辑关系搞得没必要要的复杂。因此之后UE就算想在这一方面优化增强,应该也会比较克制。
索性再聊开一些,咱们用Unity来作一下对比。Unity就是GameObject+Component,你本身组合去吧,很是的灵活自由,也不作什么限制,但形成的后果就是经常各类Component互相引用来引用去,网状互联一团乱麻。另外几乎每一个人均可以在上面搞出一套游戏系统出来,互相之间都是自成一派。因此常常网上就会有各类帖子问怎么在Unity中实现MVC模式的,也有分析炉石传说游戏逻辑框架的。Unity固然是个好引擎,目前来讲热度也是比UE要高一些,但咱们也不能由于它火用得人多,就权威崇拜从众的认为Unity各个方面都比别的引擎好。设计架构游戏的时候,工程师们要抵挡住灵活性的诱惑,保持克制每每是更可贵珍贵的美德。要认识到,引擎的终极目的是方便人使用的,咱们程序员每每很容易太沉迷于程序功能的灵活强大,而疏忽了易用性鲁棒性等社会工程需求。
思考:为什么Controller不能像Actor层级嵌套?
咱们都知道Actor能够藉着身上的SceneComponent互相嵌套。那么AController一样也是Actor,为什么不也实现这么一个父子机制?从功能上来讲,一个Controller能够有子Controllers,听起来也是很是灵活强大啊。可是冷静想一下,Controller表达的“控制”的概念,因此在这里你实际上想要表达的是一种“控制”互相嵌套的概念,感受又给“控制”给分了层,有“大控制”,也有“小控制”,可是“控制”的“大小”又是个什么概念呢?咱们应该怎么划分控制的大小?“控制”本质上来讲就是一些代码,无论怎么设计,目的都是用来表达游戏游戏逻辑的。而针对游戏逻辑的复杂,怎么更好的管理组织逻辑代码,咱们有状态机,分层状态机,行为树,GOAL(目标导向),甚至你还能搞些神经网络遗传算法机器学习啥的。因此在咱们已经有这么多工具的基础上,徒增复杂性是很危险的作法。若是有必要,也能够把Controller自己再看成其余AI算法的容器,因此就不必在对象层次上再作文章了。
思考:Controller能够显示吗?
既然Actor自己能够带着Mesh组件来渲染显示,那Controller可不能够呢?是否是Controller都是不可见的?这个答案可说是也能够说不是,由于Controller自己确实就是一个特殊点的Actor而已,你依然能够在Controller中添加Mesh组件,添加别的子Actor等,因此从这个方面说Controller是有能够渲染显示的能力的。可是一个控制者毕竟只是表达一个逻辑的概念,因此为了分工明确,UE就干脆在Controller的构造函数里把本身给隐藏了:
bHidden = true; #if WITH_EDITORONLY_DATA bHiddenEd = true; #endif // WITH_EDITORONLY_DATA
事了拂衣去,深藏功与名。为了验证个人说法,读者你能够亲自在PlayController下挂一些Cube之类的Actor,而后在源码层把这两个值改成false,从新编译运行看下结果,看可否正确显示出来,这里我就不贴图了,很好玩的哦。
思考:Controller的位置有什么意义?
既然Controller自己只是控制者,那它在场景中的位置和移动有什么意义吗?Controller为什么还须要个SceneComponent?意义在于若是Controller自己有位置信息,就能够利用该信息更好的控制Pawn的位置和移动。
首先说下Controller的Rotation,这个比较好理解一点,若是我想让个人Pawn和Controller保持旋转朝向一致,由于是Controller做主控制Pawn的关系,因此Controller就得维护本身的Rotation。再来讲位置,若是Controller有本身的位置,这样在Respawn从新生成Pawn的时候,你就能够选择在当前位置建立。所以为了自动更新Controller的位置,UE还提供了一个bAttachToPawn的开关选项,默认是关闭的,UE不会自动的更新Controller的位置信息;而若是打开,就会把Controller附加到Pawn的子节点里面去,让Controller跟随Pawn来移动。你能够把这两种模式想象成一种是上帝视角在千里以外心电感应控制Pawn,另外一种是骑在Pawn肩上来指挥。
固然若是这个Controller确实只是纯朴的逻辑控制的话(如AIController),那确实位置也没什么意义。因此UE甚至还隐藏了Controller的一些更新位置的接口,尽可能避免让人手动去操纵:
private: // Hidden functions that don't make sense to use on this class. HIDE_ACTOR_TRANSFORM_FUNCTIONS(); //展开后: ////////////////////////////////////////////////////////////////////////// // Macro to hide common Transform functions in native code for classes where they don't make sense. // Note that this doesn't prevent access through function calls from parent classes (ie an AActor*), but // does prevent use in the class that hides them and any derived child classes. #define HIDE_ACTOR_TRANSFORM_FUNCTIONS() private: \ FTransform GetTransform() const { return Super::GetTransform(); } \ FVector GetActorLocation() const { return Super::GetActorLocation(); } \ FRotator GetActorRotation() const { return Super::GetActorRotation(); } \ FQuat GetActorQuat() const { return Super::GetActorQuat(); } \ FVector GetActorScale() const { return Super::GetActorScale(); } \ bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr) { return Super::SetActorLocation(NewLocation, bSweep, OutSweepHitResult); } \ bool SetActorRotation(FRotator NewRotation) { return Super::SetActorRotation(NewRotation); } \ bool SetActorRotation(const FQuat& NewRotation) { return Super::SetActorRotation(NewRotation); } \ bool SetActorLocationAndRotation(FVector NewLocation, FRotator NewRotation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr) { return Super::SetActorLocationAndRotation(NewLocation, NewRotation, bSweep, OutSweepHitResult); } \ bool SetActorLocationAndRotation(FVector NewLocation, const FQuat& NewRotation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr) { return Super::SetActorLocationAndRotation(NewLocation, NewRotation, bSweep, OutSweepHitResult); } \ virtual bool TeleportTo( const FVector& DestLocation, const FRotator& DestRotation, bool bIsATest, bool bNoCheck ) override { return Super::TeleportTo(DestLocation, DestRotation, bIsATest, bNoCheck); } \ virtual FVector GetVelocity() const override { return Super::GetVelocity(); } \ float GetHorizontalDistanceTo(AActor* OtherActor) { return Super::GetHorizontalDistanceTo(OtherActor); } \ float GetVerticalDistanceTo(AActor* OtherActor) { return Super::GetVerticalDistanceTo(OtherActor); } \ float GetDotProductTo(AActor* OtherActor) { return Super::GetDotProductTo(OtherActor); } \ float GetHorizontalDotProductTo(AActor* OtherActor) { return Super::GetHorizontalDotProductTo(OtherActor); } \ float GetDistanceTo(AActor* OtherActor) { return Super::GetDistanceTo(OtherActor); }
UE这里其实想说的是,这些更新位置的操做仍是让我来为你管理吧,我真的担忧你会用错搞出什么乱子来。顺便再说些题外话,对于PlayerController来讲,由于玩家须要一个视角来观察世界,因此经常PlayerController经常会扛着个摄像机出现(蓝图里没有,可是会运行时生成PlayerCameraManager和CameraActor),因此就算没有角色可供操做,玩家也依然但愿是能够视角漫游观察整个世界的(试试看把默认Level里的PlayerStart删掉后运行看看)。这个时候PlayerController经常会默认建立出一个ASpectatorPawn或者DefaultPawn(根据GameMode里配置),咱们虽然看不见Pawn,但依然能够观察世界,靠得就是跟Controller关联的旋转和摄像机。
思考:哪些逻辑应该写在Controller中?
如同当初咱们在思考Actor和Component的逻辑划分同样,咱们也得要划分哪些逻辑应该放在Pawn中,哪些应该放在Contrller中。上文咱们也说过,Pawn也能够接收用户输入事件,因此其实只要你愿意,你甚至能够脱离Controller作一个特立独行的Pawn。那么在那些时候须要Controller?哪些逻辑应该由Controller掌管呢?能够从如下一些方面考虑:
咱们上文提到过Controller但愿也能有一些记忆,保存住一些游戏状态。那么到底应该怎么保存呢?AController自身固然能够添加成员变量来保存,这些变量也能够网络复制,通常来讲也够用。可是终究仍是遗忘了一个最重要的数据状态。整个游戏世界构建起来就是为了玩家服务的,而玩家在游戏过程当中,确定要存取产生一些状态。而Controller做为游戏业务逻辑最重要的载体,势必要和玩家的状态打交道。因此Controller若是能够动态存取玩家的状态就会大为方便了。所以咱们会在Controller中见到:
/** PlayerState containing replicated information about the player using this controller (only exists for players, not NPCs). */ UPROPERTY(replicatedUsing=OnRep_PlayerState, BlueprintReadOnly, Category="Controller") class APlayerState* PlayerState;
而APlayerState的继承体系是:
至于为啥APlayerState是从AActor派生的AInfo继承下来的,咱们聪明的读者相信也能猜获得了,因此也就不费口舌论证了。无非就是贪图AActor自己的那些特性以网络复制等。而AInfo们正是这种不爱表现的纯数据书呆子们的大本营。而这个PlayerState咱们能够经过在GameMode中配置的PlayerStateClass来自动生成。
注意,这个APlayerState也理所固然是生成在Level中的,跟Pawn和Controller是平级的关系,Controller里只不过保存了一个指针引用罢了。注释里说的PlayerState只为players存在,不为NPC生成,指的是PlayerState是跟UPlayer对应的,换句话说当前游戏有多少个真正的玩家,才会有多少个PlayerState,而那些AI控制的NPC由于不是真正的玩家,因此也不须要建立生成PlayerState。可是UE把PlayerState的引用变量放在了Controller一级,而不是PlayerController之中,说明了其实AIController也是能够设置读取该变量的。一个AI智能可以读取玩家的比分等状态,有了更多的信息来做决策,想来也没有什么不对嘛。
Controller和网络的结合很紧密,不少机制和网络也很是强关联,可是在这里并不详细叙述,这里先能够单纯理解成Controller也能够看成玩家在服务器上的代理对象。把PlayerState独立构成一个Actor还有一个好处,当玩家偶尔因网络波动断线,由于这个链接不在了,因此该Controller也失效了被释放了,服务器能够把对应的该PlayerState先暂存起来,等玩家再紧接着重连上了,能够利用该PlayerState从新挂接上Controller,以此提供一个比较顺畅无缝的体验。至于AIController,由于都是运行在Server上的,Client上并无,因此也就无所谓了。
思考:哪些数据应该放在PlayerState中?
从应用范围上来讲,PlayerState表示的是玩家的游玩数据,因此那些关卡内的其余游戏数据就不该该放进来(GameState是个好选择),另外Controller自己运行须要的临时数据也不该该归PlayerState管理。而玩家在切换关卡的时候,APlayerState也会被释放掉,全部PlayerState实际上表达的是当前关卡的玩家得分等数据。这样,那些跨关卡的统计数据等就也不该该放进PlayerState里了,应该放在外面的GameInstance,而后用SaveGame保存起来。
在游戏里,若是要评劳模,那Controller们无疑是最兢兢业业的,虽然有时候蛮横霸道了一些,可是常常工做在第一线,下面的Pawn们经常智商过低,上面的Level,GameMode们又有点高高在上,让他们直接管理数量繁多的Pawn们又有点太折腾,因而事无巨细的真正干那些脏活累活的还得靠Controller们。本文虽然没有在网络一块留太多笔墨,可是Controller也是同时做为联机环境中最重要的沟通渠道,身兼要职。
回顾总结一下本文要点,UE在Pawn这个层级演化构成了一个最基本和很是完善的Component-Actor-Pawn-Controller的结构:
经过分化出来后的Actor的互相控制,既充分利用了现有的机制功能,又提供了足够的灵活性,并且作的更改还不多,不用再设计额外另外一套框架。读者朋友们,如今咱们若是翻到第一小节,想一想UE最初从Object分化出Actor的那一刻,是否是有不少感慨和感动呢?一个最初的很简单的游戏对象表示,慢慢演化派生充实起来,彼此之间通力配合,竟也能优雅的运转起来。
有时候架构的设计和搭建是一脉相承的,最初的时候选择了什么样的模型和骨架,后面再设计别的逻辑框架等其余模块,也基本上都得跟最初的设计配合着来。因此有时候每每也会发现,怎么感受我架构设计的方案可选择数量并很少啊?实际上是由于若是一开始铺垫的好,接下来的设计水到渠成天然而然,让你感受不到用心设计的力气。UE以Actor的视角来看待世间万物,天然获得的是一个Actor繁荣昌盛的世界;Unity以Component来组装万物,获得的就是个各类插件组件组装出的世界;而若是如Cocos2dx通常万物都是Node,那么天然也会获得一棵挂满各类Node的世界之树。这也算是游戏引擎的基因吧。
本想着一篇介绍完Controller、PlayerController和AIController这三个对象,可是Controller自己是UE里极为重要的核心概念,自身的功能很是的丰富,牵扯的模块也比较多,所以想抽离阐述最核心的概念和功能并非一件容易的事。花了这么长的篇幅,只讨论揣摩了Controller的设计过程和最基本的职责(还有输入网络等都没有解释),顺便先简单介绍了下PlayerState出场(PlayerState其实是跟UPlayer关联更大一些,PlayerController等后续章节会继续讨论它),对于PlayerController和AIController,目前也只是语焉不详的含糊带过。不过仍是但愿读者们能从中吸收到设计的养分,把握清楚概念了,才能更好的组织游戏逻辑,开发出更好的游戏。
本系列教程的一个重点也是尝试介绍引擎各类概念背后的考量,而不是单纯的叙述解释各个模块功能。笔者始终认为,只有咱们愿意不吝口舌的去讨论,愿意耐下心来去思考学习,这些概念的领悟才会了然在心中。不然若只是单纯的介绍Pawn功能有123,Controller能够ABC,相信读者在阅读完以后也并不会有什么深的印象,由于这些只是设计的结果,少了设计的过程。
而下篇咱们将隆重介绍Controller家族中最耀眼的明星、上帝的宠儿:PlayerController!
UE4的版本更新实在太快,为了留下版本存照和供读者查证,之后在篇尾都会标注上本文研究使用的源码版本。之后再也不特地作此声明。
UE 4.13.2
我的原创,未经受权,谢绝转载!