蓝图简化技巧

 射线
可以在Pawn里设置一根专用的拾取射线,检测频率20Hz即可:拾取结果需要缓存下来,以供其他模块查询。结果包含:指向的对象类型(UCLASS)、对象引用. 同时,应该实现一个手柄Ray函数,输入是射线长度,输出是射线的Start(world)、End(world)、Direction(world)、LocalDirection,方便后续调用发出射线。如果有某些情境下需要修改射线属性的操作,则还需要增加这些接口。因此总结起来,有一个射线类,属性有:射线类型(single obj、multiple objs、channels)、ignore objects、check types/channels、check result(上面说的两个属性:UCLASS、UObject/AActor/UActorComponent引用等)
 获取可以动态创建的对象
如果动态创建对象在应用场景里是允许的,那么就应该动态创建对象,尽量不要在BeginPlay或其他Init里将其初始化,凡是要初始化时,语义就是离散的了,思维不集中。典型的处理方式是:定义一个Query/GetXXX函数,该函数返回一个XXX实例。函数的实现是先检测XXX的存在性,若存在则直接返回,若不存在则先创建并初始化后再返回。这里还有个细节可以优化一下:如果XXX实例是蓝图Context的,可以用Macro来实现这个函数,并设置两根输出,一个then,表示返回的实例有效,一个Fail,表示XXX实例创建失败。如果XXX实例是Global Context的,则没有更好的办法了,需要在QueryXXX的调用处额外判断一下XXX实例的有效性。
 对象的显隐
这种,一般情况下,在对象显示时,还需要设定其正确的位置和姿态。而隐藏时,最好将对象置于z=-100的地下,避免对WidgetInteraction和射线检测造成副作用。解决这个问题的最好方式是:创建一个函数一次性解决这些问题,函数名叫SetHiddenXXX(obj, hidden)。函数第一件事有可能是检测对象存在性,不存在则先创建对象。在对象存在的前提下,再检测hidden,若为true,则hide对象先,并紧接着调用SetWorldLocation(0,0,-100)将其深埋地下。若hidden为false,则先计算对象的正确位置和姿态,然后将该对象变换到目标位置和目标姿态,然后show出对象。
 强化版FindLookAtRotation
原生的Find Look at Rotation返回的是3D自由空间的Rotation,强化版需要指定参数是否约束Pitch,这个需求很多时候都要用。(keyword: lookat) 实现很简单:(TargetLocation - SourceLocation) * (1,1,0) -> Make Rot From X即可。或者(SourceLocation - TargetLocation) * (-1,-1,0) -> Make Rot From X
 计算TargetLocation
典型的需求描述有2种:
 同时计算TargetLocation和TargetRotation
把上面的强化版FindLookAtRotation和计算TargetLocation组合成一个函数即可
 已知向量的局部系表示,求其世界系表示
Parent.T * delta
 已知向量的世界系表示,求其局部系表示
Parent.T.Inverse * delta. 类似的有Parent.Rotation.UnrotateVector(delta)
 sequence播放长度问题
有时很多需求都是在特定时间点,用sequence来实现的。这种应用场景,典型的问题是后面的逻辑,即后面的逻辑依赖sequence播完后来做或者播放多长时间后来做,这本身没什么问题。 问题是这些时间都是在蓝图里硬编码的,不利于需求频繁变更时的快速调整。解决这个问题应该制作一个sequence播放管理器集中管理全部的sequence播放。 这个Manager应该提供sequence注册功能,注册的参数要表明多长时间后要触发什么事件(即便如需要强制停止播放的操作也可以通过抛出事件,在事件里停止播放来实现), 抛出的事件里要能区分注册者、sequence身份,事件名字,事件参数。典型的实现:实现一个全局ManagerActor,每个sequence register时都设置定时器,内部tick检测注册列表中每个sequence的播放状态(unplay、playing、pause、stop、playcount)。 ManagerActor提供一个EventDispatcher,参数是(UObject、sequence、event_name),每处向ManagerActor注册sequence的地方,都要Bind Event到ManagerActor的EventDispatcher,并在事件实现里首先通过UObject参数检测event_name是否自己注册的。

Look At Function

 蓝图节点【Look At Function】解释
从上面的描述可以看到,无论如何,该节点要达成的目标都是让Look at Vector轴与ToTarget对齐。UpVector与Clamp Cone in Degree都只是可能导致ToTarget进行调整。
至于为什么要求Look at Vector是固定的一根轴,其直觉理解就是本节点的每次计算都是从对象处于零旋转姿态的起点开始的。而Look at Vector轴也是在这个零旋转姿态下给出的轴向描述,比如:要让对象顶部去追踪一个目标的话,则Look at Vector应设定为(0,0,1),每次计算都是从零姿到Target的直接求解,即便是在一个持续追踪的运动中。如果你用GetActorUpVector()节点输出到Look at Vector,由于GetActorUpVector()的值可能一直在变化,导致Look At Function节点会认为你不同帧中想让对象零姿下的不同部位朝向目标,而Look At想要达成的效果通常是想让对象零姿下的某个固定部位一直朝向目标,因此最终导致的效果是对象不停的大幅度翻转闪烁。

旋转相关的一些思考

针对物体当前的姿态,以及从当前姿态变化到另一个姿态的变化量,我们没有用两种不同的描述形式来区分这两个概念。即,姿态与姿态的变化量虽然是两个不同的概念,但是用同一种形式描述的。也有把姿态叫做方位的。不管怎样,也可以找到一个角度,从这个角度出发可以说姿态与姿态的变化量是同一个事物。姿态的变化量是指从一个姿态到另一个姿态的变化量,而姿态也可以定义为从一个全局唯一初始姿态到当前的姿态的变化量。因此,这种理解下,它们就有了用相同的描述形式来进行描述的逻辑根据。
FTransform(Location(0,0,0), Rotation(0,0,0), Scale3D(1,1,1))就是这个初始姿态。通常叫Identity Transform,这里简称为ID、零姿态注意:这里蕴含着在一个明确的参照系里定义一个姿态,并将该姿态的数值描述记为Identity Transform
当用一个FTransform描述一个物体当前的姿态时,应该把这个Transform理解为从零姿态到当前姿态的变化量。
旋转的表示,不管是欧拉角(FRotator)还是四元数(FQuat),针对的都只是在坐标原点的旋转。四元数的轴角对表示中,旋转轴也是指穿过坐标原点的轴。
如果要表达在世界其他点旋转,则要结合位移来表达。FTransform有3个分量,分别是Location、Rotation、Scale3D。对一个具体的FTransform来说,其Location与Rotation是互相独立的,比如FTransform(FVector(100,0,0), FRotator(0,0,90)),其描述的是物体从所处世界的零姿态开始旋转90度后,平移到该世界的(100,0,0)点,那么它看起来就像是在(100,0,0)处的物体原地旋转了90度。因此,这个物体的姿态,就不能只用旋转来表达,必须将位置与旋转结合起来表达。FTransform(FVector::ZeroVector, FRotator(0,0,90))表达的是在原点转了90度的物体。
 案例1:如果物体要在当前姿态T0的基础上额外在原地进行一个世界旋转D(FRotator表达),则物体最终的姿态T1=FTransform(T0.Location(), T0.Rotation() * D)。想一想D为0时的特殊情形加深理解。上面也说了,Location与Rotation是互相独立的部分,因此旋转先全部执行到位,就是T0.Rotation()*D了,然后执行位移,由于这里变换只有原地旋转,因此位移保持不变,就是T0.Location()了。这里有一个很重要的问题:D是否就是T0.Invert() * T1? 答案为否。
FTransform的乘法(见TransformVectorized.h的Multiply函数),作用就是在姿态T0的基础上进行增量变换DeltaTransform,最后变成姿态T1。这个变换过程可以直观的理解:在保持物体处于T0姿态的前提下,给它添加一个父节点构成刚性连接,且该父节点处于零姿态(世界原点、无初始旋转),对该父节点应用DeltaTransform,由于刚性连接,该父节点会带动物体也进行完全相同的DeltaTransform变换。因此,很明显,物体最终的姿态就是T1=FTransform(NewLocation, NewRotation),其中:
  • NewRotation=T0.Rotation() * DeltaTransform.Rotation()
  • NewLocation=DeltaTransform.Rotation().Rotate(T0.Location()) + DeltaTransform.Location()
回到案例1最后的问题。案例1中T1的计算过程,明显不是FTransform的乘法实现,因此那个世界旋转D并不是T0.Invert() * T1。要知道T0*Delta=T1,这个Delta才是T0.Invert()*T1,而T1不是由T0*D实现的,因此Delta != D
 案例2:在案例1中,如果该物体还有一个父节点,但整个刚性构造需要以该物体作为中心进行变换,求父节点变换后的姿态?
在案例2这个变换过程中,父节点反而变成该物体逻辑上的子节点被带动。由于是刚性连接,父节点与该物体具有完全相同的变化量:绕相同的轴进行了相同量的旋转,以及进行了相同的平移。如果想求出父节点在变换后的姿态,则先求出物体的变化量即可:DeltaTransform = T0.Invert() * T1。这里要注意:求出DeltaTransform后,不能用Parent->AddWorldTransform(DeltaTransform)来求出父节点变换后的姿态,因为AddWorldTransform执行的并不是FTransform的乘法。需要通过Parent->GetWorldTransform()*DeltaTransform来求得父节点最终的姿态。这是方法一。
那么能否通过Parent->AddWorldTransform(D)的方式求出父节点的姿态呢?也不行。因为仅靠D只能实现父节点原地旋转,并不是绕物体进行变换。需要增加位移信息,用NewRotation=Parent->WorldRotation()*D作为旋转信息,用D.Rotate(Parent->WorldLocation()-Object->WorldLocation())+Object->WorldLocation()求出父节点的位移信息,即可求出正确的姿态,而这个实现其实就是FTransform的乘法过程。
以上思考均在UE蓝图里验证正确。