想要啥Component,Actor你本身拿html
若是让你来制做一款3D游戏引擎,你会怎么设计其结构?c++
尽管游戏的类型有不少种,市面上也有众多的3D游戏引擎,但绝大部分游戏引擎都得解决一个基本问题:抽象模拟一个3D游戏世界。根据基本的图形学知识,咱们知道,为了展现这个世界,咱们须要一个个带着“变换”的“游戏对象”,接着让它们父子嵌套以表现更复杂的结构。本质上,其余的物理模拟,游戏逻辑等功能组件,最终目的也只是为了操做这些“游戏对象”。
这件事,在Unity那里就直接成了“GameObject”和“Component”;在Cocos2dx那里是一个个的“CCNode”,操纵部分直接内嵌在了CCNode里面;在Medusa里是一个个“INode”和“IComponent”。
那么在UE4的眼中,它是怎么看待游戏的3D世界的?微信
UE创世,万物皆UObject,接着有Actor。网络
起初,UE创世,有感于天地间C++原始之气一片混沌虚无,便撷取凝实一团C++之气,降下无边魔力,洒下秩序之光,便为这个世界生成了坚实的土壤UObject,并用UClass一一为此命名。
架构
世界有了土壤以后,但还少了一些生动色彩,若是女娲造人通常,UE取一些UObject的泥巴,派生出了Actor。在UE眼中,整个世界今后了有了一个个生动的“演员”,众多的“演员”们,一块儿齐心合力为观众上演一场精彩的游戏。
app
脱胎自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 FVector GetActorLocation(const T* RootComponent) {
return (RootComponent != nullptr) ? RootComponent->GetComponentLocation() : FVector(0.f,0.f,0.f);
}
bool AActor::SetActorLocation(const FVector& NewLocation, bool bSweep, FHitResult* OutSweepHitResult, ETeleportType Teleport)
{
if (RootComponent)
{
const FVector Delta = NewLocation - GetActorLocation();
return RootComponent->MoveComponent(Delta, GetActorQuat(), bSweep, OutSweepHitResult, MOVECOMP_NoFlags, Teleport);
}
else if (OutSweepHitResult)
{
*OutSweepHitResult = FHitResult();
}
return false;
}复制代码
同理,Actor能接收处理Input事件的能力,其实也是转发到内部的UInputComponent* InputComponent;一样也提供了便利方法。ide
世界纷繁复杂,光有一种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不能互相嵌套?而在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来建立父子链接的。
void AActor::AttachToActor(AActor* ParentActor, const FAttachmentTransformRules& AttachmentRules, FName SocketName)
{
if (RootComponent && ParentActor)
{
USceneComponent* ParentDefaultAttachComponent = ParentActor->GetDefaultAttachComponent();
if (ParentDefaultAttachComponent)
{
RootComponent->AttachToComponent(ParentDefaultAttachComponent, AttachmentRules, SocketName);
}
}
}
void AActor::AttachToComponent(USceneComponent* Parent, const FAttachmentTransformRules& AttachmentRules, FName SocketName)
{
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()
enum class EAttachmentRule : 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实例化的时候才真正建立。
void UChildActorComponent::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());
}
}
else if (ChildActorClass)
{
CreateChildActor();
}
}
void UChildActorComponent::OnComponentCreated()
{
Super::OnComponentCreated();
CreateChildActor();
}复制代码
这就致使了一个问题,当你把一个ActorClass拖进Level后,这个Actor实际是已经实例化了,你能够直接调整这个Actor的属性。可是你把它拖到另外一个Actor Class里,它只会给你空空白白的ChildActorComponent的DetailsPanel,你想调整Actor的属性,就只能等生成了以后,用蓝图或代码去修改。这一点来讲,其实仍是挺不方便的,我我的以为应该是还有优化的空间。
UE终于听到了人民群众的呼声,在4.14里增长了Child Actor Templates来支持在子ChildActor的DetailsPannel里查看和修改属性。
花了这么多篇幅,才刚刚讲到Actor和Component这两个最基本的总体设计,而关于Actor,Component生命周期,Tick,事件传递等机制性的问题,还都没有展开。UE做为从1代至今4代,久经磨练的一款成熟引擎,GamePlay框架部分其实也就不到十个类,而这些类之间怎么组织,为啥这么设计,有什么权衡和考虑,我相信这里面实际上是很是有讲究的。若是是UE的总架构师来说解的话,确定能有很是多的心得体会故事。而咱们做为学习者,也应该尽可能去体会琢磨它的用心,一方面磨练咱们本身的架构设计能力,一方面也让咱们更能掌握这个游戏的引擎。
今后篇开始,会按部就班的探讨各个部分的结构设计,最后再从总体的框架上讨论该结构的优劣点。
下一篇预告:GamePlay架构(二)Level和World
知乎专栏:InsideUE4
UE4深刻学习QQ群:456247757(非新手入门群,请先学习完官方文档和视频教程)
微信公众号:aboutue,关于UE的一切新闻资讯、技巧问答、文章发布,欢迎关注。
我的原创,未经受权,谢绝转载!