C++初心者がUE4の挙動を覚えるために無謀にもエンジンソースの解析を試みた記事になります。
C++初心者ゆえ、間違いがあると思うのでやんわりと指摘していただけると助かります。
今回はSetActorRotationを解析してみます。
検証は4.15プレビュー版で行っています。
将来処理が変わる可能性大なので、参考程度にしてください。
#SetActorRotationを解析してみる
BP上でのSetActorRotationを見てみましょう。
BPでのSetActorRotationノードにあたる、C++のメソッドは以下だと推測されます。
/**
* Set the Actor's rotation instantly to the specified rotation.
*
* @param NewRotation The new rotation for the Actor.
* @param bTeleportPhysics Whether we teleport the physics state (if physics collision is enabled for this object).
* If true, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
* If false, physics velocity is updated based on the change in position (affecting ragdoll parts).
* @return Whether the rotation was successfully set.
*/
UFUNCTION(BlueprintCallable, meta=(DisplayName = "SetActorRotation"), Category="Utilities|Transformation")
bool K2_SetActorRotation(FRotator NewRotation, bool bTeleportPhysics);
bool AActor::K2_SetActorRotation(FRotator NewRotation, bool bTeleportPhysics)
{
return SetActorRotation(NewRotation, TeleportFlagToEnum(bTeleportPhysics));
}
BPとC++の引数との違いを書いてみました。
ピン | BP引数名 | BP型名 | C++引数名 | C++型名 |
---|---|---|---|---|
入力 | New Rotation | Rotator | NewRotation | FRotator |
入力 | Teleport Physics | Boolean | bTeleportPhysics | bool |
※BP型名はエディター上で確認できる型名となります。
K2_SetActorRotationの引数は全て入力ピンのみです。
c++のクラスにもSetActorRotationメソッドがあるみたいです。
TeleportはTeleportFlagToEnumという名前のメソッドを呼んで相手のメソッドに渡しているみたいです。
ここはSetActorLocationと同様の処理のようです。
FORCEINLINE ETeleportType TeleportFlagToEnum(bool bTeleport) { return bTeleport ? ETeleportType::TeleportPhysics : ETeleportType::None; }
K2_SetActorRotationの解析はさっと終わったので、SetActorRotationメソッドの中身を読んでみます。
/**
* Set the Actor's rotation instantly to the specified rotation.
*
* @param NewRotation The new rotation for the Actor.
* @param Teleport How we teleport the physics state (if physics collision is enabled for this object).
* If equal to ETeleportType::TeleportPhysics, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
* If equal to ETeleportType::None, physics velocity is updated based on the change in position (affecting ragdoll parts).
* @return Whether the rotation was successfully set.
*/
bool SetActorRotation(FRotator NewRotation, ETeleportType Teleport = ETeleportType::None);
bool SetActorRotation(const FQuat& NewRotation, ETeleportType Teleport = ETeleportType::None);
bool AActor::SetActorRotation(FRotator NewRotation, ETeleportType Teleport)
{
#if ENABLE_NAN_DIAGNOSTIC
if (NewRotation.ContainsNaN())
{
logOrEnsureNanError(TEXT("AActor::SetActorRotation found NaN in FRotator NewRotation"));
NewRotation = FRotator::ZeroRotator;
}
#endif
if (RootComponent)
{
return RootComponent->MoveComponent(FVector::ZeroVector, NewRotation, true, nullptr, MOVECOMP_NoFlags, Teleport);
}
return false;
}
bool AActor::SetActorRotation(const FQuat& NewRotation, ETeleportType Teleport)
{
#if ENABLE_NAN_DIAGNOSTIC
if (NewRotation.ContainsNaN())
{
logOrEnsureNanError(TEXT("AActor::SetActorRotation found NaN in FQuat NewRotation"));
}
#endif
if (RootComponent)
{
return RootComponent->MoveComponent(FVector::ZeroVector, NewRotation, true, nullptr, MOVECOMP_NoFlags, Teleport);
}
return false;
}
なん……だと……?
まさかの同名クラスが二つあるとは……。
違いはNewRotationがポインタ第一引数がFRotator型かFQuatの定数参照型ですね。
BPから呼び出されるSetActorRotationメソッドはポインタ無しFRotator型のほうです。
なのでもう一つのSetActorRotationは解析を後回しにします。
自分用メモ。alweiさんから後者のSetActorRotationについて
@YuukiOgino SetActorRotaionのオーバーロード版ですが、const &はポインターではなく、定数の参照型ですね。定数なので中身を触ることは出来ませんが、参照型なのでメモリーコピーもなく高速に渡すことが可能です。
— alwei (@aizen76) 2017年2月13日
@YuukiOgino で、よくみると後者はクォータニオンを渡しているので、同じ回転でもやっていることは全然違いますね。
— alwei (@aizen76) 2017年2月13日
必要ないと思いますが、BPでのSetActorRotationとC++のSetActorRotation(ポインタ無し)の引数の違いです。
ピン | BP引数名 | BP型名 | C++引数名 | C++型名 |
---|---|---|---|---|
入力 | New Rotation | Rotator | NewRotation | FRotator |
入力 | Teleport Physics | Boolean | Teleport | ETeleportType |
まずは#ifで処理を判定しているようですね。
ENABLE_NAN_DIAGNOSTICという名前からNaNチェックを有効にしているのかどうかの判定だと思われます。
つづいてENABLE_NAN_DIAGNOSTICの定義を探してみます。
//#define IMPLEMENT_ASSIGNMENT_OPERATOR_MANUALLY
// Assert on non finite numbers. Used to track NaNs.
#ifndef ENABLE_NAN_DIAGNOSTIC
#define ENABLE_NAN_DIAGNOSTIC 0
#endif
ENABLE_NAN_DIAGNOSTICが引っかかるのはこの部分だけであり、これを見る限り、エンジン拡張しない限りは有効にならないように見えます。
もし有効化した場合、NewRotation.ContainsNaN()の処理は以下のようになっています。
FORCEINLINE bool FQuat::ContainsNaN() const
{
return (!FMath::IsFinite(X) ||
!FMath::IsFinite(Y) ||
!FMath::IsFinite(Z) ||
!FMath::IsFinite(W)
);
}
FMath::IsFiniteを呼び出しているので、有限の値かどうかを判定しているようです。
ついでにIsFiniteの中身を見てみましょう。
static FORCEINLINE bool IsFinite( float A ) { return _finite(A) != 0; }
_Check_return_ _ACRTIMP int __cdecl _finite(_In_ double _X);
#define _In_ _SAL2_Source_(_In_, (), _Pre1_impl_(__notnull_impl_notref) _Pre_valid_impl_ _Deref_pre1_impl_(__readaccess_impl_notref))
うん、なんだか泥沼にダイブしたようですね。
ここまでくると正直何をしているのかわかりません。
とりあえず有限かどうかチェックして、有限でない場合はLogMacrosを呼び出してエラーを表示させた後、ZeroRotatorをセットしているようです。
LogMacrosの中身を見てみましょう。
// Macro to either log an error or ensure on a NaN error.
#if DO_CHECK && !USING_CODE_ANALYSIS
namespace UE4Asserts_Private
{
CORE_API void VARARGS InternalLogNANDiagnosticMessage(const TCHAR* FormattedMsg, ...); // UE_LOG(LogCore, Error, _FormatString_, ##__VA_ARGS__);
}
#define logOrEnsureNanError(_FormatString_, ...) \
if (!GEnsureOnNANDiagnostic)\
{\
if (UE4Asserts_Private::TrueOnFirstCallOnly([]{}))\
{\
UE4Asserts_Private::InternalLogNANDiagnosticMessage(_FormatString_, ##__VA_ARGS__); \
}\
}\
else\
{\
ensureMsgf(!GEnsureOnNANDiagnostic, _FormatString_, ##__VA_ARGS__); \
}
#else
#define logOrEnsureNanError(_FormatString_, ...)
#endif // DO_CHECK
名前からもわかるとおり、マクロですね。
とりあえず画面へ出力してるんだなということがわかったので、簡単に終わらせます。
NaNチェックをしない場合は、MoveComponentメソッドを呼び出しているようです。
MoveComponentメソッド以降の呼び出しはSetActorLocationと同じのようです。
※追記 すみません、よく見たら参照先が違っていました
SetActorLocationと同じ、MoveComponentメソッドを呼び出しますが、参照先が違うようです。
// FRotator version. This could be a simple wrapper to the FQuat version, but in the case of no significant change in location or rotation (as FRotator),
// we avoid passing through to the FQuat version because conversion can generate a false negative for the rotation equality comparison done using a strict tolerance.
bool USceneComponent::MoveComponent(const FVector& Delta, const FRotator& NewRotation, bool bSweep, FHitResult* Hit, EMoveComponentFlags MoveFlags, ETeleportType Teleport)
{
if (GetAttachParent() == nullptr)
{
if (Delta.IsZero() && NewRotation.Equals(RelativeRotation, SCENECOMPONENT_ROTATOR_TOLERANCE))
{
if (Hit)
{
Hit->Init();
}
return true;
}
return MoveComponentImpl(Delta, RelativeRotationCache.RotatorToQuat_ReadOnly(NewRotation), bSweep, Hit, MoveFlags, Teleport);
}
return MoveComponentImpl(Delta, NewRotation.Quaternion(), bSweep, Hit, MoveFlags, Teleport);
}
まず、AttachParentがnullptrか否か判定しています。
/** What we are currently attached to. If valid, RelativeLocation etc. are used relative to this object */
private:
UPROPERTY(ReplicatedUsing = OnRep_AttachParent)
USceneComponent* AttachParent;
FORCEINLINE USceneComponent* USceneComponent::GetAttachParent() const
{
return AttachParent;
}
どうやら親に付属してない場合のみ、特別な処理が走るようです。
続いてのif文を解析します。
最初のDelta.IsZero()は渡された引数がFVector::ZeroVectorであるため、確実にtrueです。
続いてRotator.EqualsでBPのNewRotationがSCENECOMPONENT_ROTATOR_TOLERANCE、つまり1.e-4fと等しいかチェックします。
#define SCENECOMPONENT_ROTATOR_TOLERANCE (1.e-4f) // Comparison tolerance for checking if two FRotators are the same when moving SceneComponents.
もしtrueならHitがnullptrでないならhitからinitメソッドが呼ばれていますが、SetActorRotationは確実にnullなので行われません。
最後にtrueがリターンされて処理が終了です。
そうでない場合は、SetActorLocationと同じくMoveComponentImplが呼ばれます。
bool USceneComponent::MoveComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit, EMoveComponentFlags MoveFlags, ETeleportType Teleport)
{
SCOPE_CYCLE_COUNTER(STAT_MoveComponentSceneComponentTime);
// static things can move before they are registered (e.g. immediately after streaming), but not after.
if (IsPendingKill() || CheckStaticMobilityAndWarn(SceneComponentStatics::MobilityWarnText))
{
if (OutHit)
{
*OutHit = FHitResult();
}
return false;
}
// Fill in optional output param. SceneComponent doesn't sweep, so this is just an empty result.
if (OutHit)
{
*OutHit = FHitResult(1.f);
}
ConditionalUpdateComponentToWorld();
// early out for zero case
if( Delta.IsZero() )
{
// Skip if no vector or rotation.
if (NewRotation.Equals(ComponentToWorld.GetRotation(), SCENECOMPONENT_QUAT_TOLERANCE))
{
return true;
}
}
// just teleport, sweep is supported for PrimitiveComponents. This will update child components as well.
const bool bMoved = InternalSetWorldLocationAndRotation(GetComponentLocation() + Delta, NewRotation, false, Teleport);
// Only update overlaps if not deferring updates within a scope
if (bMoved && !IsDeferringMovementUpdates())
{
// need to update overlap detection in case PrimitiveComponents are attached.
UpdateOverlaps();
}
return true;
}
ただ前回同様、時間がなくてMoveComponentImplメソッドがまだ解析しきれてないです。
こちらはまた後日解析します。
これで大体、自分なりにSetActorRotationの挙動を理解しました。
#まとめ
座標を変えるメソッドは、最終的にMoveComponentImplメソッドを呼び出して変えているということがわかりました。
おそらく他の座標を変更する処理もMoveComponentImplメソッドが呼ばれていると推測できます。
引き続きUE4のエンジンソースを読んでいきたいと思います