はじめに
こんにちは。
キャラクターの原点について気にした事はありますか?
UE標準のキャラクター(ACharacter)は、原点がカプセルの中心となっています。
これを足元に変更したいのですが、ACharacterのCapsuleComponentはルートコンポーネントなので
カプセルのオフセットを変更することができません。
「pivot capsule」あたりで検索すると、同じような悩みがちらほら。
https://forums.unrealengine.com/t/change-pivot-of-acharacter/588714
https://forums.unrealengine.com/t/how-hard-would-it-be-to-create-a-child-class-of-ucapsulecomponent-that-aligns-from-the-bottom/568889
https://forums.unrealengine.com/t/inherited-capsule-component-placed-inside-ground/385271/3
今回は、このキャラクターの原点を足元にする
1つの手法についてご紹介します。
やり方
完成品イメージ。
自作 CharacterMovmentComponentを作成する
- UCharacterMovmentComponentを継承して「UMyCharacterMovmentComponent」を作成します。
- MoveComponentImplをオーバーライドします。
UCLASS()
class UMyCharacterMovementComponent : public UCharacterMovementComponent
{
GENERATED_BODY()
public:
bool SyncCapsuleMovementToRoot(const FVector& Delta, const FQuat& NewRotation, bool bSweep,
FHitResult* Hit = nullptr,
EMoveComponentFlags MoveFlags = MOVECOMP_NoFlags,
ETeleportType Teleport = ETeleportType::None);
protected:
virtual bool MoveUpdatedComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit = 0, ETeleportType Teleport = ETeleportType::None) override;
};
- SyncCapsuleMovementToRootは以下のように実装します。
- MoveUpdatedComponentImplはSyncCapsuleMovementToRootを呼ぶようにします。
※ 解説は後程
bool UMyCharacterMovementComponent::SyncCapsuleMovementToRoot(const FVector& Delta, const FQuat& NewRotation,
bool bSweep,
FHitResult* Hit, EMoveComponentFlags MoveFlags,
ETeleportType Teleport)
{
bool bMoved = false;
if (UpdatedComponent)
{
// 1. カプセルを移動する
const FVector NewDelta = ConstrainDirectionToPlane(Delta);
bMoved = UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, Hit, MoveFlags, Teleport);
if (bMoved && GetWorld()->IsGameWorld())
{
if (UCapsuleComponent* CapsuleComponent = Cast<UCapsuleComponent>(UpdatedComponent))
{
// 2. カプセルの位置をリセットする
FVector CapsuleOffset(0, 0, CapsuleComponent->GetScaledCapsuleHalfHeight());
CapsuleComponent->SetRelativeLocation_Direct(CapsuleOffset);
CapsuleComponent->SetRelativeRotation_Direct(FRotator::ZeroRotator);
// 3. カプセルの移動結果をRootComponentに反映する
if (USceneComponent* RootComponent = GetOwner()->GetRootComponent())
{
FTransform MovedTransform = CapsuleComponent->GetComponentTransform();
FVector NewLocation = MovedTransform.GetLocation() - MovedTransform.GetRotation().RotateVector(CapsuleOffset);
RootComponent->SetWorldLocationAndRotationNoPhysics(NewLocation,MovedTransform.Rotator());
}
}
}
}
return bMoved;
}
bool UMyCharacterMovementComponent::MoveUpdatedComponentImpl(const FVector& Delta, const FQuat& NewRotation,
bool bSweep, FHitResult* OutHit, ETeleportType Teleport)
{
return SyncCapsuleMovementToRoot(Delta, NewRotation, bSweep, OutHit, EMoveComponentFlags::MOVECOMP_NoFlags, Teleport);
}
自作 RootComponentを作成する
- USceneComponentを継承して「UMyRootComponent」を作成します。
- MoveComponentImplをオーバーライドします。
- メンバーにUMyCharacterMovementComponentへのポインタを所持しておきます。
UCLASS(Blueprintable)
class UMyCharacterRootComponent : public USceneComponent
{
GENERATED_BODY()
public:
UPROPERTY(Transient)
TObjectPtr<UMyCharacterMovementComponent> CharaMove = nullptr;
protected:
virtual bool MoveComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* Hit = 0, EMoveComponentFlags MoveFlags = MOVECOMP_NoFlags, ETeleportType Teleport = ETeleportType::None) override;
};
- MoveComponentImpl内ではGameWorldの時のみ、UMyCharacterMovementComponentのSyncCapsuleMovementToRootを呼ぶようにします。
※ 解説は後程
bool UMyCharacterRootComponent::MoveComponentImpl(const FVector& Delta, const FQuat& NewRotation, bool bSweep,
FHitResult* Hit, EMoveComponentFlags MoveFlags,
ETeleportType Teleport)
{
if (CharaMove && GetWorld()->IsGameWorld())
{
return CharaMove->SyncCapsuleMovementToRoot(Delta, NewRotation, bSweep, Hit, MoveFlags, Teleport);
}
return Super::MoveComponentImpl(Delta, NewRotation, bSweep, Hit, MoveFlags, Teleport);
}
自作 ACharacterを作成する
- ACharacterを継承して「AMyCharacter」を作成します。
- ObjectInitializer.SetDefaultSubobjectClassで「UMyCharacterMovementComponent」を指定し、作成した物が生成されるようにします。
AMyCharacter::AMyCharacter(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UMyCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
ObjectInitializerについては、こちらが大変参考になります。
AMyCharacterのコンストラクタの一番最後に下記コードを追加します。
// 自前RootComponentを生成し、ルートコンポーネントに設定する
UMyCharacterRootComponent* MyRootComponent = CreateDefaultSubobject<UMyCharacterRootComponent>(TEXT("MyRoot"));
SetRootComponent(MyRootComponent);
// メッシュとカプセルの親を変更する
GetMesh()->SetupAttachment(MyRootComponent);
GetCapsuleComponent()->SetupAttachment(MyRootComponent);
// 自前RootComponentににCharacterMovementを設定
if (UMyCharacterMovementComponent* MyCharaMove = Cast<UMyCharacterMovementComponent>(GetCharacterMovement()))
{
MyRootComponent->CharaMove = MyCharaMove;
}
BPを調整する
カプセルコンポーネントがルートコンポーネントではなくなる事で、
オフセットを編集することが可能になりました。
- MeshのZに入っている値0にします。
- CapsuleComponentのZにCapsule Half Height Offsetの値を入れます。
完成
ヤッター!
解説
RootComponentを置き換える
CapsuleComponentはルートコンポーネントなのでオフセットを設定できませんでした。
自作RootComponentに置き換えて、CapsuleComponentを子にする事で
オフセットを編集することができるようになります。
ただ、これだけでは下記のようにCapsuleComponentが独立して動いてしまい
アクターのルートが動かない状態になってしまいます。
カプセルの移動結果をルートに反映させる
オーバーライドしたMoveUpdatedComponentImplが呼ばれるタイミングは2つあります。
- CharacterMovementComponent内で移動処理が行われた時
- UMyCharacterMovementComponentのMoveUpdatedComponentImplが呼ばれる
- Actorに対してSetActorLocationなどの処理が呼ばれた時
- UMyCharacterRootComponentのMoveUpdatedComponentImplが呼ばれる
それぞれMoveUpdatedComponentImplが呼ばれた場合、
SyncCapsuleMovementToRoot関数を呼ぶように処理しています。
また、IsGameWorldで判定しているのはBP編集時に補正が行われないようにするためです。
続いてSyncCapsuleMovementToRoot内の処理について。
まず、UpdatedComponent(カプセルコンポーネント)を移動させます。
上記動画のようなカプセルだけが移動した状態になります。
// 1. カプセルを移動する
const FVector NewDelta = ConstrainDirectionToPlane(Delta);
bMoved = UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, Hit, MoveFlags, Teleport);
次に、カプセルコンポーネントのトランスフォームを直接リセットします。
~_Direct系の関数で直接値を変更することができます。
カプセルコンポーネントはアクターのルート位置に戻ります。
// 2. カプセルの位置をリセットする
FVector CapsuleOffset(0, 0, CapsuleComponent->GetScaledCapsuleHalfHeight());
CapsuleComponent->SetRelativeLocation_Direct(CapsuleOffset);
CapsuleComponent->SetRelativeRotation_Direct(FRotator::ZeroRotator);
最後に、カプセルコンポーネントが移動した結果のトランスフォームを
SetWorldLocationAndRotationNoPhysicsで、ルートコンポーネントに直接反映します。
カプセルの移動した結果から、カプセル半分の高さ分を引いて足元に合わせています。
// 3. カプセルの移動結果をRootComponentに反映する
if (USceneComponent* RootComponent = GetOwner()->GetRootComponent())
{
FTransform MovedTransform = CapsuleComponent->GetComponentTransform();
FVector NewLocation = MovedTransform.GetLocation() - MovedTransform.GetRotation().RotateVector(CapsuleOffset);
RootComponent->SetWorldLocationAndRotationNoPhysics(NewLocation, MovedTransform.Rotator());
}
つまり、カプセル君の手柄をルートが横取りする仕組みになっています。
カプセル君、おつかれさん!(肩ポン)
おわりに
いかがでしたでしょうか。
ここまで書いておいて何ですが、トリッキーな手法なのでおすすめはできません。
(もっと正攻法な手法をご存じでしたら教えていただきたいです…)
注意すべき点としては、しゃがみや、泳ぎなどのカプセルに依存している箇所で問題がでます。
中心を原点にしている処理を、ちまちま直していく必要があります。
また、「Character Mover」を検討してみても良いかもしれません。
詳しくは調べられていないのですが、ボックスコリジョンに変更できたりするようなので、原点に関しても拡張の余地があるかもしれません。というかあってほしい(願望)
CharacterMoverについては、alweiさんの記事が大変参考になります。
最終の手段として、ACharacterやCharacterMovmenetComponentを自作することですが、
現在作っている物と相談して、現実的かどうかご検討ください。
おまけ
PlayerStartもZを0に指定する必要があります。
こういうのを各所でちまちまやっていきます。
起動してみると…
なんだこの2cm…!?…0じゃないの?
これはカプセルと床にバッファを持たせるための
CharacterMovmenetComponentの仕様のようです。
こまけぇことはいいんだよ!!!
~完~