概要
こんにちは。パンダマスターと申します。
最近、UE4でゲームを作り始めました。
半年くらい前、UE4にはBPで挑戦してみたのですが、さっぱり分からなくて挫折してしまいました。
半年経ってトラウマが払しょくできたので、今度はC++プロジェクトで再挑戦と相成ります。
ある程度結果が出たので投稿してみました。
注記
この記事の内容はEpic Gamesとは何の関係もありません。
免責事項
当記事に記載されている内容、サンプルコードを使用することによって被った、いかなる損害に対しても筆者は一切責任を負いません。当記事のサンプルコードの使用に際しては自己責任にてお願いします。
上記に同意していただける方のみ、続きをお読みください。
続きを読むことによって、上記の免責事項に同意したことになります。
今回の目標
UE4 C++プロジェクトで、AIControllerを使って複数のキャラクターを動かす。
バージョン情報
Unreal Engine 4.25.2
実装
早速、実装を見ていきましょう!
まだ整理された状態ではないため、コード全体ではなく苦労した点についてかいつまんで書いていきます。
投稿中のコードはプロジェクトからの抜粋ですので、それだけでは動きません。
SkeletalMeshの設定
そもそもキャラクターの表示から始まりました。
紆余曲折を経て、下記のようなコードに落ち着きました。
パスがPuzzleになっているのはテンプレートプロジェクトがそれだからです。
ACharacterEx::ACharacterEx()
{
ConstructorHelpers::FObjectFinder<USkeleton> SkeletonAsset(TEXT("Skeleton'/Game/Puzzle/Meshes/SK_Mannequin_Skeleton.SK_Mannequin_Skeleton'"));
GetMesh()->SetSkeletalMesh(MeshAsset.Object);
GetMesh()->SetMaterial(0, /*ロードしたマテリアル*/);
}
こんだけのことに数日かかりました。
始めはUPROPERTY()で自前のプロパティにメッシュ作ってたりしたのですが、GetMeshして置き換えればいいだけでした(涙
ちなみに自前のプロパティでも表示はされますが、ControllerでMoveToLocation等で移動させてもメッシュは追随しません。
AIContorollerでの移動
TickでMoveToLocationしてるだけ。
void AAIControllerEx::BeginPlay()
{
Super::BeginPlay();
SetFocalPoint(FVector(-3000.f, -3000.f, 0.f));
}
void AIControllerEx::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// OwnerCharにACharacterExインスタンスを自前で設定していますが、備え付けのOwnerあるかもね
EPathFollowingRequestResult::Type result = MoveToLocation(OwnerChar->GetMesh()->GetRelativeLocation() + FVector(-3000.f, -3000.f, 0.f), 10.f, false, true, true);
// Goalはフラグで、後述するUAnimInstanceの継承クラス内でBPのステートマシンのトランジションに使ってます。
OwnerChar->Goal = result == EPathFollowingRequestResult::AlreadyAtGoal;
}
AIControllerの設定はAACtorクラスを継承したSpawnerで設定しています。ACharacterExのコンストラクタでやってもいいかも。
基本はAIControllerClassにStaticClass()の戻り値を設定してSpawnDefaultController()するだけ。
void AACtorEx::BeginPlay()
{
Super::BeginPlay();
const int32 NumBlocks = Size * Size;
for (int32 CharIndex = 0; CharIndex < NumChars; CharIndex++)
{
const float XOffset = (CharIndex / Size) * CharSpacing;
const float YOffset = (CharIndex % Size) * CharSpacing;
const FVector CharLocation = FVector(XOffset, YOffset, 0.f) + GetActorLocation();
ACharacterEx* NewChar = GetWorld()->SpawnActor<ACharacterEx>(CharLocation, FRotator(0, 0, 0));
FVector Rotation = FVector(XOffset, YOffset, 0.f) - FVector(-3000.f, -3000.f, 0.f);
Rotation.X = -Rotation.X;
// 2020/8/9 追記:Meshを動かすと表示位置とキャラクタ位置がずれるので修正
// NewChar->GetMesh()->SetRelativeLocationAndRotation(CharLocation, (Rotation).Rotation());
NewChar->GetMesh()->SetRelativeRotation((Rotation).Rotation());
// 2020/8/9 追記:カプセルコンポーネントの位置が実際の位置になるっぽい
NewChar->GetCapsuleComponent()->SetRelativeLocation(CharLocation);
NewChar->AIControllerClass = AAIControllerEx::StaticClass();
NewChar->SpawnDefaultController();
AAIControllerEx MyController = Cast<AAIControllerEx>(NewChar->GetController());
MyController->SetPawn(NewChar);
MyController->OwnerChar = NewChar;
}
}
AnimationBPの設定
AnimationInstanceを継承したBP作成するんですが、こちらの投稿を参考にしました。
UE4 アニメ―ションBPをC++クラス継承にしてアクセスする
まずはBPの親クラスを作る。
いくつかメソッドをオーバーライドして確認用のログを出してます。これは上記のサイトにある内容です。
肝はXXXRule()メソッドで、UFUNCTION(...)を書いてあげるとBPからトランジションイベントとして参照できます。
今回はIdleRule()がRun初期状態からIdle状態へのトランジションイベントとして使用されています。
// .h
class TEST_API UAnimInstanceEx : public UAnimInstance
{
GENERATED_BODY()
UAnimInstanceEx(const FObjectInitializer& ObjectInitializer);
virtual void NativeInitializeAnimation() override;
virtual void NativeUpdateAnimation(float DeltaTime) override;
virtual void NativePostEvaluateAnimation() override;
virtual void NativeUninitializeAnimation() override;
virtual void NativeBeginPlay() override;
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe))
bool IdleRule();
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe))
bool WalkRule();
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe))
bool RunRule();
};
// .cpp
UAnimInstanceEx::UAnimInstanceEx(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void UAnimInstanceEx::NativeInitializeAnimation()
{
Super::NativeInitializeAnimation();
UE_LOG(LogTemp, Log, TEXT("NativeInitializeAnimation()"));
}
void UAnimInstanceEx::NativeUpdateAnimation(float DeltaTime)
{
Super::NativeUpdateAnimation(DeltaTime);
UE_LOG(LogTemp, Log, TEXT("NativeUpdateAnimation()"));
}
void UAnimInstanceEx::NativePostEvaluateAnimation()
{
Super::NativePostEvaluateAnimation();
UE_LOG(LogTemp, Log, TEXT("NativePostEvaluateAnimation()"));
}
void UAnimInstanceEx::NativeUninitializeAnimation()
{
Super::NativeUninitializeAnimation();
UE_LOG(LogTemp, Log, TEXT("NativeUninitializeAnimation()"));
}
void UAnimInstanceEx::NativeBeginPlay()
{
Super::NativeBeginPlay();
UE_LOG(LogTemp, Log, TEXT("NativeBeginPlay()"));
}
bool UAnimInstanceEx::IdleRule()
{
auto _Pawn = Cast<ACharacterEx>(TryGetPawnOwner());
if (_Pawn == nullptr) {
UE_LOG(LogTemp, Log, TEXT("PawnNull"));
return(false);
}
// AAIControllerExのTickで設定したGoalフラグ
return _Pawn->Goal;
}
bool UAnimInstanceEx::RunRule()
{
return false;
}
bool UAnimInstanceEx::WalkRule()
{
return false;
}
できあがったBPをロードしてSkeletalMeshComponentに設定してあげるだけ。
ちなみに、BPクラスを右クリックしてリファレンスをコピーしてそのまま貼り付けてもロードできません。
末尾に"_C"を付けてあげないとnullが返ってきちゃいます。
ACharacterEx::ACharacterEx()
{
// リファレンスの末尾に"_C"を付ける
TSubclassOf<class UAnimInstanceEx> AnimInstance = TSoftClassPtr<UAnimInstanceEx>(FSoftObjectPath(TEXT("AnimBlueprint'/Game/Puzzle/Meshes/SK_Mannequin_Skeleton_AnimBP.SK_Mannequin_Skeleton_AnimBP_C'"))).LoadSynchronous();
if (AnimInstance != nullptr)
{
GetMesh()->SetAnimClass((UClass*)AnimInstance);
}
else
{
UE_LOG(LogTemp, Log, TEXT("UAnimInstanceEx is null"));
}
}
出来上がり
こんな感じで動きました。
スケルトンのインポートに失敗しているのか、肩が外れちゃってますけどねw
感想
色々なサイトを巡ったり、エンジンのソースコード読んだりしながら、ここまで来るのに2週間かかりました。
結構かかった気はしますが、自分でエンジンやシェーダー書く場合と比べると数十倍から数百倍の速さで出来上がると思います。
出来上がってみて思ったのは、**UE4楽しい!**ですね。
皆さんもトライしてみてください。
進捗あったらまた投稿します。