3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unreal Engine (UE)Advent Calendar 2024

Day 18

[UE5] キャラクターの原点を足元にする

Last updated at Posted at 2024-12-17

はじめに

こんにちは。
キャラクターの原点について気にした事はありますか?

UE標準のキャラクター(ACharacter)は、原点がカプセルの中心となっています。
これを足元に変更したいのですが、ACharacterのCapsuleComponentはルートコンポーネントなので
カプセルのオフセットを変更することができません。

image.png

「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つの手法についてご紹介します。

やり方

完成品イメージ。

image.png

自作 CharacterMovmentComponentを作成する

  • UCharacterMovmentComponentを継承して「UMyCharacterMovmentComponent」を作成します。
  • MoveComponentImplをオーバーライドします。
MyCharacterMovmentComponent.h
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を呼ぶようにします。

※ 解説は後程

MyCharacterMovmentComponent.cpp
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へのポインタを所持しておきます。
MyCharacterRootComponent.h
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を呼ぶようにします。

※ 解説は後程

MyCharacterRootComponent.cpp
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.cpp
AMyCharacter::AMyCharacter(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UMyCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{

ObjectInitializerについては、こちらが大変参考になります。

AMyCharacterのコンストラクタの一番最後に下記コードを追加します。

AMyCharacter.cpp
// 自前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を調整する

カプセルコンポーネントがルートコンポーネントではなくなる事で、
オフセットを編集することが可能になりました。

image.png

  • MeshのZに入っている値0にします。
  • CapsuleComponentのZにCapsule Half Height Offsetの値を入れます。

image.png

完成

ヤッター!

root_character.gif

解説

RootComponentを置き換える

CapsuleComponentはルートコンポーネントなのでオフセットを設定できませんでした。
自作RootComponentに置き換えて、CapsuleComponentを子にする事で
オフセットを編集することができるようになります。

ただ、これだけでは下記のようにCapsuleComponentが独立して動いてしまい
アクターのルートが動かない状態になってしまいます。

root_character_bad1.gif

カプセルの移動結果をルートに反映させる

オーバーライドした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に指定する必要があります。
こういうのを各所でちまちまやっていきます。

image.png

起動してみると…

…ん?
image.png

なんだこの2cm…!?…0じゃないの?

image.png

これはカプセルと床にバッファを持たせるための
CharacterMovmenetComponentの仕様のようです。

こまけぇことはいいんだよ!!!

~完~

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?