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

# はじめに

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/5bc37024-4a19-b17a-7ede-9c09a926d295.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/15aa2cd8-6fc7-18ba-0c85-3b70612256ce.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については、こちらが大変参考になります。

https://historia.co.jp/archives/6587/

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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/b84342da-52db-f23a-5204-2bc2b0c33c45.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/f29e43d2-b9bc-9723-4193-2411d23184c4.png)


## 完成

ﾔｯﾀｰ!

![root_character.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/bfaf21ab-1bb5-7be4-b25f-8abeb15c9c5c.gif)

# 解説

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

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

![root_character_bad1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/bb8d7a94-3ef3-0697-915a-5d51a3c4dd64.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さんの記事が大変参考になります。

https://unrealengine.hatenablog.com/entry/2024/03/31/180000

最終の手段として、ACharacterやCharacterMovmenetComponentを自作することですが、
現在作っている物と相談して、現実的かどうかご検討ください。

# おまけ

PlayerStartもZを0に指定する必要があります。
こういうのを各所でちまちまやっていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/9ec2665e-5f3f-dcfb-d0d1-cc28b549b325.png)

起動してみると…

…ん？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/f7691015-f327-2e1f-a8f8-a9d62e81d227.png)

なんだこの2cm…！？…0じゃないの？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/376911/2579a593-c8af-71a1-419f-60e26a551445.png)

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

https://forums.unrealengine.com/t/my-character-is-not-touching-the-floor/374497/5

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

～完～