《Inside UE4》-2-GamePlay架构(一)Actor和Component

 
 

《Inside UE4》-2-GamePlay架构(一)Actor和Component

InsideUE4

 

UE4深入学习QQ群: 456247757


 

引言

如果让你来制作一款3D游戏引擎,你会怎么设计其结构?

尽管游戏的类型有很多种,市面上也有众多的3D游戏引擎,但绝大部分游戏引擎都得解决一个基本问题:抽象模拟一个3D游戏世界。根据基本的图形学知识,我们知道,为了展示这个世界,我们需要一个个带着“变换”的“游戏对象”,接着让它们父子嵌套以表现更复杂的结构。本质上,其他的物理模拟,游戏逻辑等功能组件,最终目的也只是为了操作这些“游戏对象”。
这件事,在Unity那里就直接成了“GameObject”和“Component”;在Cocos2dx那里是一个个的“CCNode”,操纵部分直接内嵌在了CCNode里面;在Medusa里是一个个“INode”和“IComponent”。
那么在UE4的眼中,它是怎么看待游戏的3D世界的?

 

创世记

UE创世,万物皆UObject,接着有Actor。

 

UObject:

起初,UE创世,有感于天地间C++原始之气一片混沌虚无,便撷取凝实一团C++之气,降下无边魔力,洒下秩序之光,便为这个世界生成了坚实的土壤UObject,并用UClass一一为此命名。

藉着UObject提供的元数据、反射生成、GC垃圾回收、序列化、编辑器可见,Class Default Object等,UE可以构建一个Object运行的世界。(后续会有一个大长篇深挖UObject)

 

Actor:

世界有了土壤之后,但还少了一些生动色彩,如果女娲造人一般,UE取一些UObject的泥巴,派生出了Actor。在UE眼中,整个世界从此了有了一个个生动的“演员”,众多的“演员”们,一起齐心协力为观众上演一场精彩的游戏。

脱胎自Object的Actor也多了一些本事:Replication(网络复制),Spawn(生生死死),Tick(有了心跳)。
Actor无疑是UE中最重要的角色之一,组织庞大,最常见的有StaticMeshActor, CameraActor和 PlayerStartActor等。Actor之间还可以互相“嵌套”,拥有相对的“父子”关系。

思考:为何Actor不像GameObject一样自带Transform?
我们知道,如果一个对象需要在3D世界中表示,那么它必然要携带一个Transform matrix来表示其位置。关键在于,在UE看来,Actor并不只是3D中的“表示”,一些不在世界里展示的“不可见对象”也可以是Actor,如AInfo(派生类AWorldSetting,AGameMode,AGameSession,APlayerState,AGameState等),AHUD,APlayerCameraManager等,代表了这个世界的某种信息、状态、规则。你可以把这些看作都是一个个默默工作的灵体Actor。所以,Actor的概念在UE里其实不是某种具象化的3D世界里的对象,而是世界里的种种元素,用更泛化抽象的概念来看,小到一个个地上的石头,大到整个世界的运行规则,都是Actor.
当然,你也可以说即使带着Transform,把坐标设置为原点,然后不可见不就行了?这样其实当然也是可以,不过可能因为UE跟贴近C++一些的缘故,所以设计哲学上就更偏向于C++的哲学“不为你不需要的东西付代价”。一个Transform再加上附带的逆矩阵之类的表示,内存占用上其实也是挺可观的。要知道UE可是会抠门到连bool变量都要写成uint bPending:1;位域来节省一个字节的内存的。
换一个角度讲,如果把带Transform也当成一个Actor的额外能力可以自由装卸的话,那其实也可以自圆其说。经过了UE的权衡和考虑,把Transform封装进了SceneComponent,当作RootComponent。但在权衡到使用的便利性的时候,大部分Actor其实是有Transform的,我们会经常获取设置它的坐标,如果总是得先获取一下SceneComponent,然后再调用相应接口的话,那也太繁琐了。所以UE也为了我们直接提供了一些便利性的Actor方法,如(Get/Set)ActorLocation等,其实内部都是转发到RootComponent。

 
  1. /*~
  2. * Returns location of the RootComponent
  3. * this is a template for no other reason than to delay compilation until USceneComponent is defined
  4. */
  5. template<class T>
  6. static FORCEINLINE FVectorGetActorLocation(const T*RootComponent)
  7. {
  8. return(RootComponent!=nullptr)?RootComponent->GetComponentLocation():FVector(0.f,0.f,0.f);
  9. }
  10. boolAActor::SetActorLocation(constFVector&NewLocation,bool bSweep,FHitResult*OutSweepHitResult,ETeleportTypeTeleport)
  11. {
  12. if(RootComponent)
  13. {
  14. constFVectorDelta=NewLocation-GetActorLocation();
  15. returnRootComponent->MoveComponent(Delta,GetActorQuat(), bSweep,OutSweepHitResult, MOVECOMP_NoFlags,Teleport);
  16. }
  17. elseif(OutSweepHitResult)
  18. {
  19. *OutSweepHitResult=FHitResult();
  20. }
  21. returnfalse;
  22. }

同理,Actor能接收处理Input事件的能力,其实也是转发到内部的UInputComponent* InputComponent;同样也提供了便利方法。

 

Component

世界纷繁复杂,光有一种Actor可不够,自然就需要有各种不同技能的Actor各司其职。在早期的远古时代,每个Actor拥有的技能都是与生俱有,只能父传子一代代的传下去。随着游戏世界的越来越绚丽,需要的技能变得越来越多和频繁改变,这样一组合,唯出身论的Actor数量们就开始爆炸了,而且一个个也越来越胖,最后连UE这样的神也管理不了了。终于,到了第4个纪元,UE窥得一丝隔壁平行宇宙Unity的天机。下定决心,让Actor们轻装上阵,只提供一些通用的基本生存能力,而把众多的“技能”抽象成了一个个“Component”并提供组装的接口,让Actor随用随组装,把自己武装成一个个专业能手。

看见UActorComponent的U前缀,是不是想起了什么?没错,UActorComponent也是基础于UObject的一个子类,这意味着其实Component也是有UObject的那些通用功能的。(关于Actor和Component之间Tick的传递后续再细讨论)

下面我们来细细看一下Actor和Component的关系:
TSet<UActorComponent*> OwnedComponents 保存着这个Actor所拥有的所有Component,一般其中会有一个SceneComponent作为RootComponent。
TArray<UActorComponent*> InstanceComponents 保存着实例化的Components。实例化是个什么意思呢,就是你在蓝图里Details定义的Component,当这个Actor被实例化的时候,这些附属的Component也会被实例化。这其实很好理解,就像士兵手上拿着把武器,当我们拥有一队士兵的时候,自然就一一对应拥有了不同实例化的武器。但OwnedComponents里总是最全的。ReplicatedComponents,InstanceComponents可以看作一个预先的分类。

一个Actor若想可以被放进Level里,就必须实例化USceneComponent* RootComponent。但如果你光看代码的话,OwnedComponents其实也是可以包容多个不同SceneComponent的,然后你可以动态获取不同的SceneComponent来当作RootComponent,只不过这种用法确实不太自然,而且也得非常小心维护不同状态,不推荐如此用。在我们的直觉印象里,一个封装过后的Actor应该是一个整体,它能被放进Level中,拥有变换,这一整个整体的概念更加符合自然意识,所以我想,这也是UE为何要在Actor里一一对应一个RootComponent的原因。

再来说说Component下面的家族(为了阐明概念,只列出了最常见的):

ActorComponent下面最重要的一个Component就非SceneComponent莫属了。SceneComponent提供了两大能力:一是Transform,二是SceneComponent的互相嵌套。

思考:为何ActorComponent不能互相嵌套?而在SceneComponent一级才提供嵌套?
首先,ActorComponent下面当然不是只有SceneComponent,一些UMovementComponent,AIComponent,或者是我们自己写的Component,都是会直接继承ActorComponent的。但很奇怪的是,ActorComponent却是不能嵌套的,在UE的观念里,好像只有带Transform的SceneComponent才有资格被嵌套,好像Component的互相嵌套必须和3D里的transform父子对应起来。
老实说,如果让我来设计Entity-Component模式,我很可能会为了通用性而在ActorComponent这一级直接提供嵌套,这样所有的Component就与生俱来拥有了组合其他Component的能力,灵活性大大提高。但游戏引擎的设计必然也经过了各种权衡,虽然说架构上显得并不那么的统一干净,但其实也大大减少了被误用的机会。实体组件模式推崇的“组合优于继承”的概念确实很强大,但其实同时也带来了一些问题,如Component之间如何互相依赖,如何互相通信,嵌套过深导致的接口便利损失和性能损耗,真正一个让你随便嵌套的组件模式可能会在使用上更容易出问题。
从功能上来说,UE更倾向于编写功能单一的Component(如UMovementComponent),而不是一个整合了其他Component的大管家Component(当然如果你偏要这么干,那UE也阻止不了你)。
而从游戏逻辑的实现来说,UE也是不推荐把游戏逻辑写在Component里面,所以你其实也没什么机会去写一个很复杂的Component.

思考:Actor的SceneComponent哲学
很多其他游戏引擎,还有一种设计思路是“万物皆Node”。Node都带变换。比如说你要设计一辆汽车,一种方式是车身作为一个Node,4个轮子各为车身的子Node,然后移动父Node来前进。而在UE里,一种很可能的方式就变成,汽车是一个Actor,车身作为RootComponent,4个轮子都作为RootComponent的子SceneComponent。请读者们细细体会这二者的区别。两种方式都可以实现出优秀的游戏引擎,只是有些理念和侧重点不同。
从设计哲学上来说,其实你把万物看成是Node,或者是Component,并没有什么本质上的不同。看作Node的时候,Node你就要设计的比较轻量廉价,这样才能比较没有负担的创建多个,同理Component也是如此。Actor可以带多个SceneComponent来渲染多个Mesh实体,同样每个Node带一份Mesh再组合也可以实现出同样效果。
个人观点来说,关键的不同是在于你是怎么划分要操作的实体的粒度的。当看成是Node时,因为Node身上的一些通用功能(事件处理等),其实我们是期望着我们可以非常灵活的操作到任何一个细小的对象,我们希望整个世界的所有物体都有一些基本的功能(比如说被拾取),这有点完美主义者的思路。而注重现实的人就会觉得,整个游戏世界里,有相当大一部分对象其实是不那么动态的。比如车子,我关心的只是整体,而不是细小到每一个车轱辘。这种理念就会导成另外一种设计思路:把要操作的实体按照功能划分,而其他的就尽量只是最简单的表示。所以在UE里,其实是把5个薄薄的SceneComponent表示再用Actor功能的盒子装了起来,而在这个盒子内部你可以编写操作这5个对象的逻辑。换做是Node模式,想编写操作逻辑的话,一般就来说就会内化到父Node的内部,不免会有逻辑与表现掺杂之嫌,而如果Node要把逻辑再用组合分离开的话,其实也就转化成了某种ScriptComponent。

思考:Actor之间的父子关系是怎么确定的?
你应该已经注意到了Actor里面的TArray<AActor*> Children字段,所以你可能会期望看到Actor:AddChild之类的方法,很遗憾。在UE里,Actor之间的父子关系却是通过Component确定的。同一般的Parent:AddChild操作原语不同,UE里是通过Child:AttachToActor或Child:AttachToComponent来创建父子连接的。

 
  1. voidAActor::AttachToActor(AActor*ParentActor,constFAttachmentTransformRules&AttachmentRules,FNameSocketName)
  2. {
  3. if(RootComponent&&ParentActor)
  4. {
  5. USceneComponent*ParentDefaultAttachComponent=ParentActor->GetDefaultAttachComponent();
  6. if(ParentDefaultAttachComponent)
  7. {
  8. RootComponent->AttachToComponent(ParentDefaultAttachComponent,AttachmentRules,SocketName);
  9. }
  10. }
  11. }
  12. voidAActor::AttachToComponent(USceneComponent*Parent,constFAttachmentTransformRules&AttachmentRules,FNameSocketName)
  13. {
  14. if(RootComponent&&Parent)
  15. {
  16. RootComponent->AttachToComponent(Parent,AttachmentRules,SocketName);
  17. }
  18. }

3D世界里的“父子”关系,我们一般可能会认为就是3D世界里的变换的坐标空间“父子”关系,但如果再度扩展一下,如上所述,一个Actor可是可以带有多个SceneComponent的,这意味着一个Actor是可以带有多个Transform“锚点”的。创建父子时,你到底是要把当前Actor当作对方哪个SceneComponent的子?再进一步,如果你想更细控制到Attach到某个Mesh的某个Socket(关于Socket Slot,目前可以简单理解为一个虚拟插槽,提供变换锚点),你就更需要去寻找到特定的变换锚点,然后Attach的过程分别在Location,Roator,Scale上应用Rule来计算最后的位置。

 
  1. /** Rules for attaching components - needs to be kept synced to EDetachmentRule */
  2. UENUM()
  3. enumclassEAttachmentRule: uint8
  4. {
  5. /** Keeps current relative transform as the relative transform to the new parent. */
  6. KeepRelative,
  7. /** Automatically calculates the relative transform such that the attached component maintains the same world transform. */
  8. KeepWorld,
  9. /** Snaps transform to the attach point */
  10. SnapToTarget,
  11. };

所以Actor父子之间的“关系”其实隐含了许多数据,而这些数据都是在Component上提供的。Actor其实更像是一个容器,只提供了基本的创建销毁,网络复制,事件触发等一些逻辑性的功能,而把父子的关系维护都交给了具体的Component,所以更准确的说,其实是不同Actor的SceneComponent之间有父子关系,而Actor本身其实并不太关心。

接下来的左侧派生链依次提供了物理,材质,网格最终合成了一个我们最普通常见的StaticMeshComponent。而右侧的ChildActorComponent则是提供了Component之下再叠加Actor的能力。

聊一聊ChildActorComponent
同作为最常用到的Component之一,ChildActorComponent担负着Actor之间互相组合的胶水。这货在蓝图里静态存在的时候其实并不真正的创建Actor,而是在之后Component实例化的时候才真正创建。

 
  1. voidUChildActorComponent::OnRegister()
  2. {
  3. Super::OnRegister();
  4. if(ChildActor)
  5. {
  6. if(ChildActor->GetClass()!=ChildActorClass)
  7. {
  8. DestroyChildActor();
  9. CreateChildActor();
  10. }
  11. else
  12. {
  13. ChildActorName=ChildActor->GetFName();
  14. USceneComponent*ChildRoot=ChildActor->GetRootComponent();
  15. if(ChildRoot&&ChildRoot->GetAttachParent()!=this)
  16. {
  17. // attach new actor to this component
  18. // we can't attach in CreateChildActor since it has intermediate Mobility set up
  19. // causing spam with inconsistent mobility set up
  20. // so moving Attach to happen in Register
  21. ChildRoot->AttachToComponent(this,FAttachmentTransformRules::SnapToTargetNotIncludingScale);
  22. }
  23. // Ensure the components replication is correctly initialized
  24. SetIsReplicated(ChildActor->GetIsReplicated());
  25. }
  26. }
  27. elseif(ChildActorClass)
  28. {
  29. CreateChildActor();
  30. }
  31. }
  32. voidUChildActorComponent::OnComponentCreated()
  33. {
  34. Super::OnComponentCreated();
  35. CreateChildActor();
  36. }

这就导致了一个问题,当你把一个ActorClass拖进Level后,这个Actor实际是已经实例化了,你可以直接调整这个Actor的属性。但是你把它拖到另一个Actor Class里,它只会给你空空白白的ChildActorComponent的DetailsPanel,你想调整Actor的属性,就只能等生成了之后,用蓝图或代码去修改。这一点来说,其实还是挺不方便的,我个人觉得应该是还有优化的空间。也或许是我还没有体会到此设计的妙处,等以后懂了再回来填坑。

 

后记

花了这么多篇幅,才刚刚讲到Actor和Component这两个最基本的整体设计,而关于Actor,Component生命周期,Tick,事件传递等机制性的问题,还都没有展开。UE作为从1代至今4代,久经磨练的一款成熟引擎,GamePlay框架部分其实也就不到十个类,而这些类之间怎么组织,为啥这么设计,有什么权衡和考虑,我相信这里面其实是非常有讲究的。如果是UE的总架构师来讲解的话,肯定能有非常多的心得体会故事。而我们作为学习者,也应该尽量去体会琢磨它的用心,一方面磨练我们自己的架构设计能力,一方面也让我们更能掌握这个游戏的引擎。
从此篇开始,会循序渐进的探讨各个部分的结构设计,最后再从整体的框架上讨论该结构的优劣点。

下一篇预告:GamePlay框架(二)

 

UE4深入学习QQ群: 456247757

 

引用

  1. Unreal Engine 4 Terminology
  2. Gameplay Framework Quick Reference

个人原创,未经授权,谢绝转载,否则将追究法律责任!

《Inside UE4》-2-GamePlay架构(一)Actor和Component的更多相关文章

  1. 【UE4】GamePlay架构

    新标签打开或者下载看大图 更新: 增加 编程子系统 Subsystem 思维导图 Character pipeline

  2. 《Inside UE4》目录

    <Inside UE4>目录 InsideUE4 UE4无疑是非常优秀的世界上最顶尖的引擎之一,性能和效果都非常出众,编辑器工作流也非常的出色,更难得宝贵的是完全的开源让我们有机会去从中吸 ...

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

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

  4. 《Inside UE4》-1-基础概念

    <Inside UE4>-1-基础概念   InsideUE4   创建测试项目 接上文的准备工作,双击生成的UE4Editor.exe,选择创建测试C++空项目Hello(以后的源码分析 ...

  5. 《Inside UE4》-0-开篇

    <Inside UE4>-0-开篇 InsideUE4   前言 VR行业发展是越来越火热了,硬件设备上有HTC VIVE,Oculus rift,PS VR,各种魔镜:平台上有Steam ...

  6. UE4 Runtime下动态给Actor添加组件

    http://www.v5xy.com/?p=858 UE4的组件分为两种:USceneComponent, UActorComponent UActorComponent (NewObject(th ...

  7. ue4 动态增删查改 actor,bp

    ue4.17 增 特殊说明:创建bp时,如果bp上随手绑一个cube,那么生成到场景的actor只执行构造不执行beginPlay,原因未知 ATPlayerPawn是c++类 直接动态创建actor ...

  8. 【UE4 C++】Actor 与 Component —— 创建、销毁

    Actor的生成与销毁 创建Actor实例 UClass* TSubclassOf<T> SpawnActor() UPROPERTY(EditAnywhere, Category = & ...

  9. [UE4]使用另一个相机Scene Capture Component 2D当小地图

    挂一个相机(Scene Capture Component 2D)在人物角色的正上方,相机朝下,让UI上的某一块区域看到相机所显示的内容. 一.在人物角色正上方添加相机组件Scene Capture ...

随机推荐

  1. 使用 HTML5 Canvas 绘制出惊艳的水滴效果

    HTML5 在不久前正式成为推荐标准,标志着全新的 Web 时代已经来临.在众多 HTML5 特性中,Canvas 元素用于在网页上绘制图形,该元素标签强大之处在于可以直接在 HTML 上进行图形操作 ...

  2. 【特别推荐】几款极好的 JavaScript 下拉列表插件

    表单元素让人爱恨交加.作为网页最重要的组成部分,表单几乎无处不在,从简单的邮件订阅.登陆注册到复杂的需要多页填写的信息提交功能,表单都让开发者花费了大量的时间和精力去处理,以期实现好用又漂亮的表单功能 ...

  3. 免费的 Photoshop Apple Watch 原型设计素材

    大量的扁平化的苹果设备原型展示了响应式的 Web 设计.这是一组免费的 Photoshop Apple Watch 原型 PSD 设计素材,文件包括 iPhone.iPad. iMac 和 Macbo ...

  4. Eclipse中的Web项目自动部署到Tomcat

    原因 很长时间没用Eclipse了,近期由于又要用它做个简单的JSP项目,又要重新学习了,虽然熟悉的很快,但记忆总是很模糊,偶尔犯错,以前很少写博客,现在感觉还是很有必要的,编程中每个人对于犯过的错误 ...

  5. jQuery原型方法each使用和源码分析

    jQuery.each方法是jQuery的核心工具方法之一,通用例遍方法,可用于例遍对象和数组.不同于例遍 jQuery 对象的 $().each() 方法,此方法可用于例遍任何对象.通常需要两个参数 ...

  6. MyEclipse 2015免费在线公开课,2月5日开讲

    MyEclipse 2015免费在线公开课,2月5日开讲,由MyEclipse官方高级PM Brian Fernandes 主讲. 主讲内容: 更好地支持javascript和技术模块 全新的REST ...

  7. iOS coreData问题

    iOS常见错误-CoreData: Cannot load NSManagedObjectModel.nil is an illegal URL parameter
这是因为在工程中CoreData的 ...

  8. 关于alpha透明度

    使用场景 一个半透明的框上面   放了一些label     改变半透明view的alpha    结果 label的  alpha 也跟着改变了. 其实不用那么麻烦   给你们UI  要一个色值也可 ...

  9. UISegmentedControl(人物简介)

    效果图 当你点击上面人物名字的时候 ,就可以随意切换人物. 这个很有趣 , 你还可以试着添加音乐播放器 .以及一些别的来完善你想做的. 好吧 , 废话不多说 , 上代码. #import " ...

  10. iOS-公司开发者账号的申请和注册(博主原创+亲身经历+2016年申请+附带与邓白氏公司的往来邮件截图)

    不吹不黑,此篇博客真乃我的良心之作啊,希望对大家有所帮助! 链接在简书:http://www.jianshu.com/p/9de6a8eb4d88