《Inside UE4》-2-GamePlay架构(一)Actor和Component
《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。
/*~
* Returns location of the RootComponent
* this is a template for no other reason than to delay compilation until USceneComponent is defined
*/
template<class T>
static FORCEINLINE FVectorGetActorLocation(const T*RootComponent)
{
return(RootComponent!=nullptr)?RootComponent->GetComponentLocation():FVector(0.f,0.f,0.f);
}
boolAActor::SetActorLocation(constFVector&NewLocation,bool bSweep,FHitResult*OutSweepHitResult,ETeleportTypeTeleport)
{
if(RootComponent)
{
constFVectorDelta=NewLocation-GetActorLocation();
returnRootComponent->MoveComponent(Delta,GetActorQuat(), bSweep,OutSweepHitResult, MOVECOMP_NoFlags,Teleport);
}
elseif(OutSweepHitResult)
{
*OutSweepHitResult=FHitResult();
}
returnfalse;
}
同理,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来创建父子连接的。
voidAActor::AttachToActor(AActor*ParentActor,constFAttachmentTransformRules&AttachmentRules,FNameSocketName)
{
if(RootComponent&&ParentActor)
{
USceneComponent*ParentDefaultAttachComponent=ParentActor->GetDefaultAttachComponent();
if(ParentDefaultAttachComponent)
{
RootComponent->AttachToComponent(ParentDefaultAttachComponent,AttachmentRules,SocketName);
}
}
}
voidAActor::AttachToComponent(USceneComponent*Parent,constFAttachmentTransformRules&AttachmentRules,FNameSocketName)
{
if(RootComponent&&Parent)
{
RootComponent->AttachToComponent(Parent,AttachmentRules,SocketName);
}
}
3D世界里的“父子”关系,我们一般可能会认为就是3D世界里的变换的坐标空间“父子”关系,但如果再度扩展一下,如上所述,一个Actor可是可以带有多个SceneComponent的,这意味着一个Actor是可以带有多个Transform“锚点”的。创建父子时,你到底是要把当前Actor当作对方哪个SceneComponent的子?再进一步,如果你想更细控制到Attach到某个Mesh的某个Socket(关于Socket Slot,目前可以简单理解为一个虚拟插槽,提供变换锚点),你就更需要去寻找到特定的变换锚点,然后Attach的过程分别在Location,Roator,Scale上应用Rule来计算最后的位置。
/** Rules for attaching components - needs to be kept synced to EDetachmentRule */
UENUM()
enumclassEAttachmentRule: uint8
{
/** Keeps current relative transform as the relative transform to the new parent. */
KeepRelative,
/** Automatically calculates the relative transform such that the attached component maintains the same world transform. */
KeepWorld,
/** Snaps transform to the attach point */
SnapToTarget,
};
所以Actor父子之间的“关系”其实隐含了许多数据,而这些数据都是在Component上提供的。Actor其实更像是一个容器,只提供了基本的创建销毁,网络复制,事件触发等一些逻辑性的功能,而把父子的关系维护都交给了具体的Component,所以更准确的说,其实是不同Actor的SceneComponent之间有父子关系,而Actor本身其实并不太关心。
接下来的左侧派生链依次提供了物理,材质,网格最终合成了一个我们最普通常见的StaticMeshComponent。而右侧的ChildActorComponent则是提供了Component之下再叠加Actor的能力。
聊一聊ChildActorComponent
同作为最常用到的Component之一,ChildActorComponent担负着Actor之间互相组合的胶水。这货在蓝图里静态存在的时候其实并不真正的创建Actor,而是在之后Component实例化的时候才真正创建。
voidUChildActorComponent::OnRegister()
{
Super::OnRegister();
if(ChildActor)
{
if(ChildActor->GetClass()!=ChildActorClass)
{
DestroyChildActor();
CreateChildActor();
}
else
{
ChildActorName=ChildActor->GetFName();
USceneComponent*ChildRoot=ChildActor->GetRootComponent();
if(ChildRoot&&ChildRoot->GetAttachParent()!=this)
{
// attach new actor to this component
// we can't attach in CreateChildActor since it has intermediate Mobility set up
// causing spam with inconsistent mobility set up
// so moving Attach to happen in Register
ChildRoot->AttachToComponent(this,FAttachmentTransformRules::SnapToTargetNotIncludingScale);
}
// Ensure the components replication is correctly initialized
SetIsReplicated(ChildActor->GetIsReplicated());
}
}
elseif(ChildActorClass)
{
CreateChildActor();
}
}
voidUChildActorComponent::OnComponentCreated()
{
Super::OnComponentCreated();
CreateChildActor();
}
这就导致了一个问题,当你把一个ActorClass拖进Level后,这个Actor实际是已经实例化了,你可以直接调整这个Actor的属性。但是你把它拖到另一个Actor Class里,它只会给你空空白白的ChildActorComponent的DetailsPanel,你想调整Actor的属性,就只能等生成了之后,用蓝图或代码去修改。这一点来说,其实还是挺不方便的,我个人觉得应该是还有优化的空间。也或许是我还没有体会到此设计的妙处,等以后懂了再回来填坑。
后记
花了这么多篇幅,才刚刚讲到Actor和Component这两个最基本的整体设计,而关于Actor,Component生命周期,Tick,事件传递等机制性的问题,还都没有展开。UE作为从1代至今4代,久经磨练的一款成熟引擎,GamePlay框架部分其实也就不到十个类,而这些类之间怎么组织,为啥这么设计,有什么权衡和考虑,我相信这里面其实是非常有讲究的。如果是UE的总架构师来讲解的话,肯定能有非常多的心得体会故事。而我们作为学习者,也应该尽量去体会琢磨它的用心,一方面磨练我们自己的架构设计能力,一方面也让我们更能掌握这个游戏的引擎。
从此篇开始,会循序渐进的探讨各个部分的结构设计,最后再从整体的框架上讨论该结构的优劣点。
下一篇预告:GamePlay框架(二)
UE4深入学习QQ群: 456247757
引用
个人原创,未经授权,谢绝转载,否则将追究法律责任!
《Inside UE4》-2-GamePlay架构(一)Actor和Component的更多相关文章
- 【UE4】GamePlay架构
新标签打开或者下载看大图 更新: 增加 编程子系统 Subsystem 思维导图 Character pipeline
- 《Inside UE4》目录
<Inside UE4>目录 InsideUE4 UE4无疑是非常优秀的世界上最顶尖的引擎之一,性能和效果都非常出众,编辑器工作流也非常的出色,更难得宝贵的是完全的开源让我们有机会去从中吸 ...
- 《InsideUE4》GamePlay架构(十)总结
世界那么大,我想去看看 引言 通过对前九篇的介绍,至此我们已经了解了UE里的游戏世界组织方式和游戏业务逻辑的控制.行百里者半九十,前述的篇章里我们的目光往往专注在于特定一个类或者对象,一方面固然可以让 ...
- 《Inside UE4》-1-基础概念
<Inside UE4>-1-基础概念 InsideUE4 创建测试项目 接上文的准备工作,双击生成的UE4Editor.exe,选择创建测试C++空项目Hello(以后的源码分析 ...
- 《Inside UE4》-0-开篇
<Inside UE4>-0-开篇 InsideUE4 前言 VR行业发展是越来越火热了,硬件设备上有HTC VIVE,Oculus rift,PS VR,各种魔镜:平台上有Steam ...
- UE4 Runtime下动态给Actor添加组件
http://www.v5xy.com/?p=858 UE4的组件分为两种:USceneComponent, UActorComponent UActorComponent (NewObject(th ...
- ue4 动态增删查改 actor,bp
ue4.17 增 特殊说明:创建bp时,如果bp上随手绑一个cube,那么生成到场景的actor只执行构造不执行beginPlay,原因未知 ATPlayerPawn是c++类 直接动态创建actor ...
- 【UE4 C++】Actor 与 Component —— 创建、销毁
Actor的生成与销毁 创建Actor实例 UClass* TSubclassOf<T> SpawnActor() UPROPERTY(EditAnywhere, Category = & ...
- [UE4]使用另一个相机Scene Capture Component 2D当小地图
挂一个相机(Scene Capture Component 2D)在人物角色的正上方,相机朝下,让UI上的某一块区域看到相机所显示的内容. 一.在人物角色正上方添加相机组件Scene Capture ...
随机推荐
- 使用 HTML5 Canvas 绘制出惊艳的水滴效果
HTML5 在不久前正式成为推荐标准,标志着全新的 Web 时代已经来临.在众多 HTML5 特性中,Canvas 元素用于在网页上绘制图形,该元素标签强大之处在于可以直接在 HTML 上进行图形操作 ...
- 【特别推荐】几款极好的 JavaScript 下拉列表插件
表单元素让人爱恨交加.作为网页最重要的组成部分,表单几乎无处不在,从简单的邮件订阅.登陆注册到复杂的需要多页填写的信息提交功能,表单都让开发者花费了大量的时间和精力去处理,以期实现好用又漂亮的表单功能 ...
- 免费的 Photoshop Apple Watch 原型设计素材
大量的扁平化的苹果设备原型展示了响应式的 Web 设计.这是一组免费的 Photoshop Apple Watch 原型 PSD 设计素材,文件包括 iPhone.iPad. iMac 和 Macbo ...
- Eclipse中的Web项目自动部署到Tomcat
原因 很长时间没用Eclipse了,近期由于又要用它做个简单的JSP项目,又要重新学习了,虽然熟悉的很快,但记忆总是很模糊,偶尔犯错,以前很少写博客,现在感觉还是很有必要的,编程中每个人对于犯过的错误 ...
- jQuery原型方法each使用和源码分析
jQuery.each方法是jQuery的核心工具方法之一,通用例遍方法,可用于例遍对象和数组.不同于例遍 jQuery 对象的 $().each() 方法,此方法可用于例遍任何对象.通常需要两个参数 ...
- MyEclipse 2015免费在线公开课,2月5日开讲
MyEclipse 2015免费在线公开课,2月5日开讲,由MyEclipse官方高级PM Brian Fernandes 主讲. 主讲内容: 更好地支持javascript和技术模块 全新的REST ...
- iOS coreData问题
iOS常见错误-CoreData: Cannot load NSManagedObjectModel.nil is an illegal URL parameter 这是因为在工程中CoreData的 ...
- 关于alpha透明度
使用场景 一个半透明的框上面 放了一些label 改变半透明view的alpha 结果 label的 alpha 也跟着改变了. 其实不用那么麻烦 给你们UI 要一个色值也可 ...
- UISegmentedControl(人物简介)
效果图 当你点击上面人物名字的时候 ,就可以随意切换人物. 这个很有趣 , 你还可以试着添加音乐播放器 .以及一些别的来完善你想做的. 好吧 , 废话不多说 , 上代码. #import " ...
- iOS-公司开发者账号的申请和注册(博主原创+亲身经历+2016年申请+附带与邓白氏公司的往来邮件截图)
不吹不黑,此篇博客真乃我的良心之作啊,希望对大家有所帮助! 链接在简书:http://www.jianshu.com/p/9de6a8eb4d88