e.blog

主にUnity/UE周りのことについてまとめていきます

Unreal Engine C++ 逆引きメモ

目次

概要

まだまだUEに慣れていないので、色々なことを忘れる前に逆引きでメモしておきます。
(なので、随時更新予定)

また、UEでC++を書くにあたって理解しておかなければならない点として、標準のC++とは「異なる」という点です。
どういうことかと言うと、UEではガベージコレクション、通称GCと呼ばれる仕組みを導入しています。

しかし、当然ながらC++にはガベージコレクションはありません。
そこでUEでは、独自のインスンタンス生成の仕組みや、マクロを用いたプリプロセッサ経由で様々な、GCのための準備をしてくれます。

そのため、標準のC++とUEで使うC++、言ってみればUE C++とで作法が違う、という点を覚えておく必要があります。

生成・取得・削除

C++クラスの生成

通常のC++では、newを用いてインスタンスを生成します。
しかし、UE C++ではインスタンスの生成方法に違いがあります。理由は前述のように、GC対象として管理するためです。
最近ではスマートポインタを使ったりしますが、それと似た感じですね。

UMyClass MyClass = NewObject<UMyClass>();

// オーナーを指定する場合は引数に入れる
// UMyClass MyClass = NewObject<UMyClass>(Owner);

当然のことながら、UE管理下に置かないような純粋なC++で書く処理についてはこの作法は適用されません。

コンストラクタ内でNewObjectは使えない

どうやら、コンストラクタ内では上記のNewObjectは使えないようです。(使うとクラッシュする)

ではどうするかというと、FObjectInitializer::CreateDefaultSubobjectを利用します。
FObjectInitializerは、コンストラクタ引数に指定しておくと、UEシステムが適切に渡してくれるようになっています。(詳細は後述)

以下のようにコンストラクタを定義することで利用できるようになります。

UAnyClass::UAnyClass(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // オブジェクト生成
    UAthorClass* instance = ObjectInitializer.CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

しかし実はこのCreateDefaultSubobjectは、UObject基底クラスでラッパーが実装されているようで、オブジェクトを生成する目的だけであれば、CreateDefaultSubobjectを直に呼ぶことで同様のことを達成することができます。

UAnyClass::UAnyClass()
{
    // オブジェクト生成
    UAthorClass* instance = CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

なおこのコンストラクタに引数を指定した場合としない場合の挙動の差ですが、UEシステムが自動的に生成する****.generated.h内にてマクロが生成され、コンストラクタの定義に応じて書き換わるよういなっているようです。

詳細はこちら([UE4] ObjectInitializerでコンポーネント生成を制御する | 株式会社ヒストリア)の記事をご覧ください。

PlayerController / PlayerPawnを取得する

ゲーム開始時に生成されたプレイヤーコントローラ / プレイヤーポーンを取得するには、Kismet/GameplayStatics.hを読み込む必要があります。

PlayerController

#include "Kismet/GameplayStatics.h"

APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0);
if (PlayerController)
{
    // do anything.
}

PlayerPawn

#include "Kismet/GameplayStatics.h"

// UGameplayStatics::GetPlayerPawnを介して取得し、適切にキャストする
AAnyCharacter* MyCharacter = Cast<AAnyCharacter>(UGameplayStatics::GetPlayerPawn(this, 0));
if (MyCharacter)
{
    // do anything.
}

GameModeとの関連

なお、このPlayerControllerPlayerPawnの関係は、PawnをコントロールするのがPlayerControllerの役割です。
これらの設定はGameModeに設定するようになっており、またGameModeはプロジェクト設定にて設定され、これが実行時に起動するポイントとなるようです。

詳細はこちら↓

msyasuda.hatenablog.com

ワールドに存在するアクターをすべて取得する

#include "Kismet/GameplayStatics.h"

// find all AnyActors
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AAnyActor::StaticClass(), FoundActors);

for (auto Actor : FoundActors)
{
    AAnyActor* AnyActor = Cast<AAnyActor>(Actor);
    if (AnyActor )
    {
        // do anything.
    }
}

アクターオブジェクトを生成する

オブジェクトの生成には、UWorldクラスのメソッドを利用します。

// FActorSpawnParametersを使うのに必要
#include "Runtime/Engine/Classes/Engine/World.h"

// AActor::GetWorldから、UWorldを得る
UWorld* const World = GetWorld();

// Nullチェック
if (!World)
{
    return;
}

FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotator(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = Instigator;

AAnyActor* AnyActor = World->SpawnActor<AAnyActor>(ActorBP, Location, Rotation, SpawnParams);

削除中かを知る

まだ理解が浅いですが、Unityと同様、Destroyを実行しても即座にメモリから消えるわけではなく、ゲームとして破綻しないよう様々な終了処理が存在するはずです。
そのため、削除中、という状態が存在します。
それをチェックするには以下のようにします。

AActor* anyActor = ...;
anyActor->IsPendingKill();

移動

アクターを移動させる

AActorクラスにはGetActorLocationSetActorLocationがあるのでこれを利用する。

FVector location = actor->GetActorLocation();
location.X += 10.0f;
actor->SetActorLocation(location);

ちなみに、SetActorLocationの定義を見ると以下のようになっています。

/** 
 * Move the actor instantly to the specified location. 
 * 
 * @param NewLocation  The new location to teleport the Actor to.
 * @param bSweep       Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something.
 *                     Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect.
 * @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).
 *                     If CCD is on and not teleporting, this will affect objects along the entire swept volume.
 * @param OutSweepHitResult The hit result from the move if swept.
 * @return Whether the location was successfully set if not swept, or whether movement occurred if swept.
 */
bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr, ETeleportType Teleport = ETeleportType::None);

引数を色々変更することによって、移動後の物理干渉などに対する設定や演算結果を受け取ることができるようです。

カメラの向いている方向に移動させる

まず、Worldからカメラマネージャを取得し、そこからカメラの前方を取得、それを元にアクターを移動させる、という手順で行います。

#include "Engine.h" // GEngineを使うのでインクルードしておく

void AMyActor::BeginPlay()
{
    if (GEngine != nullptr)
    {
        // CameraManagerをワールドから取得する
        CameraManager = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager;
    }
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    FRotator cameraRot = CameraManager->GetCameraRotation();
    FVector dir = cameraRot.Vector();
    dir.Normalize();

    FVector location = GetActorLocation();
    location += dir * 10.0f * DeltaTime;
    
    SetActorLocation(location);
}

コンポーネント

Blueprintに、カスタムコンポーネントを表示させる

UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))
class PROJECTNAME_API UHogeMovementComponent : public UPawnMovementComponent
{
    // ... 略 ...
}

こんな感じで、UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))を指定すると、BPのAddComponentのリストに表示されるようになります。

コンポーネントのセットアップ

コンポーネントを適切にセットアップしないと、TickComponentが呼ばれないなど問題が出るため、適切にセットアップする必要があります。

void USampleComponent::AttachSampleComponent(AActor* Actor)
{
    if (!Actor)
    {
        return;
    }

    // Componentを新規生成
    UOtherComponent* Comp = NewObject<UOtherComponent>(Actor);

    // アクタにアタッチする
    Actor->AddInstanceComponent(Comp);

    // UActorComponent::RegisterComponentで、イベントループに登録する
    Comp->RegisterComponent();
}

追加されているコンポーネントを取得する

TArray<AnyComponent*> components;
GetComponents<AnyComponent>(components);

// コンポーネントの数を確認
UE_LOG(LogTemp, Log, TEXT("Count: %d"), components.Num());

Ownerを取得する

ComponentはActorのコンポーネントとして振る舞うため、オーナーを取得して操作することが増えるかと思います。
オーナーの取得は以下のようにします。

AActor *owner = GetOwner();

ComponentからInputを使う

InputまわりはUInputComponentが司ります。
UActorComponentではUInputComponentを保持していないので、オーナーなどから取得して適切にセットアップする必要があります。

PawnクラスのサブクラスなどではSetupPlayerInputComponentのタイミングでUInputComponentが渡ってくるので、そこでセットアップの機会があるようです)

https://docs.unrealengine.com/latest/INT/Programming/Tutorials/PlayerCamera/3/docs.unrealengine.com

AActor *actor = GetOwner();
actor->InputComponent->BindAction("Fire", IE_Pressed, this, &UAnyComponent::FireHandler);

void UAnyComponent::FireHandler()
{
    UE_LOG(LogTemp, Log, TEXT("Fire!!!");
}

※ Input系については、UE4のお作法に則って適切にセットアップする必要があります。

セットアップについては、以下の引越しガイドの「入力イベント」あたりに載っています。

docs.unrealengine.com

Static Meshを生成する

USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
UStaticMeshComponent *mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("mesh"));
mesh->SetupAttachment(root);

static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("[reference]"));
UStaticMesh *asset = MeshAsset.Object;

mesh->SetStaticMesh(asset);

ちなみに、[reference]の部分は、UEエディタのアセットのコンテキストメニュー内のCopy Referenceから得ることができます。

f:id:edo_m18:20180208102926p:plain

参考:
forums.unrealengine.com

Colliderを設定する

コライダの設定にはUSphereComponentなどを利用します。
詳細パネルではSphere Collisionとか表示されるやつです。

AMyPawn::AMyPawn()
{
    // ... 中略 ...

    // Sphereコライダを生成
    USphereComponent *sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
    sphere->InitSphereRadius(100.0f);
    sphere->SetSimulatePhysics(false);
    sphere->SetCollisionProfileName(TEXT("BlockAll"));

    // Sphereの見た目を生成
    UStaticMeshComponent *sphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SphereVisual"));
    sphereVisual->AttachTo(sphere);

    static ConstructorHelpers::FObjectFinder<UStaticMesh> meshAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    if (meshAsset.Succeeded())
    {
        sphereVisual->SetStaticMesh(meshAsset.Object);
        sphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
        sphereVisual->SetWorldScale3D(FVector(0.8f));
    }
}

セットアップ時に行っているのは3点。

  • sphere->InitSphereRadius(100.0f);
  • sphere->SetSimulatePhysics(false);
  • sphere->SetCollisionProfileName(TEXT("BlockAll"));

です。それぞれ上から、

  • 球体の半径の設定
  • 物理挙動させるか(falseの場合は、Unityで言うIsKinematic = trueの状態)
  • コリジョンの仕方

となります。
特に最後の「コリジョンの仕方」は、衝突するのか、イベントのみ(overlap)なのか、というところ設定するので、意図した通りに設定しておかないと「衝突しないじゃん」となるので注意です。(というか、ここを勘違いしていてずっと衝突しなくてちょっとハマった)

コリジョンを処理する

コリジョンを設定したあとは、コリジョン発生時になにかしらの処理をしたい場合があります。
その場合に使えるのが、ColliderのもつGetOverlappingActorsメソッドです。

コライダの範囲にあるAActorオブジェクトを取得してくれます。

※ 以下のコードは、公式のチュートリアルの中で使われているコードの抜粋です。

void ABatteryCollectorCharacter::CollectPickups()
{
    // Get all overlapping Actors and store them in an array
    TArray<AActor*> CollectedActors;
    CollectionSphere->GetOverlappingActors(CollectedActors);

    // For each Actor we collected
    for (int32 iCollected = 0; iCollected < CollectedActors.Num(); ++iCollected)
    {
        // Cast the actor to APickup
        APickup* const TestPickup = Cast<APickup>(CollectedActors[iCollected]);

        // If the cast is successful and the pickup is valid and active
        if (TestPickup && !TestPickup->IsPendingKill() && TestPickup->IsActive())
        {
            // call the pickup's WasCollected function
            TestPickup->WasCollected();

            // Deactivate the pickup 
            TestPickup->SetActive(false);
        }
    }
}

Blueprintで利用する

C++で書いたクラス(コンポーネント)も、ブループリントから利用するように作成することが出来ます。
そのためのマクロがUPROPERTYUFUNCTIONです。
これらを適切に設定することで、ブループリントから設定できたり、あるいはGetter / Setterとして機能したり、あるいはブループリントで実装を促す、なんてこともできるようになります。

以下に、キーワードと意味を、よく見るものを抜粋して記載しておきます。
※ 英語ドキュメントの翻訳(意訳)なので、詳細についてはドキュメントをご覧ください。

Keyword 意味
BlueprintImplementableEvent ブループリント(ノード)でオーバーライドするように促す。そのため、Body(実装)部分は書いてはならない。UEにより、(ブループリントで)オーバーライドされた本体を実行するProcessEventを呼び出すためのコードが自動生成される
BlueprintNativeEvent BlueprintImplementableEvent同様、ブループリントでオーバーライドするようデザインされたものであるが、違いとしてはC++による実装を行う点。実装本体は[FunctionName]_Implementationを実装する必要がある。元の[FunctionName]内には、自動生成コードとして、[FunctionName]_Implementationを呼び出すコードが追加される(※1)
BlueprintPure このキーワードをつけられた関数は、副作用を起こさないものとしてマーク付けられ、さらにBlueprintCallableを意味します。Getメソッドの実装に適したものです。さらに、non-pureになるようにconst functionでfalseとマークすることもできる(※2)
BlueprintCallable Blueprintから呼び出しできるようにマークする
Category カテゴリを定義する。設定すると、Blueprint上でカテゴライズされて表示される

※1 [FunctionName]\_Implementationを実装しないとコンパイルエラーになる。定義すると、BPのEvent Graphで配置して利用できるようになる。以下、サンプル↓

UFUNCTION(BlueprintNativeEvent)
void HogeHoge();

virtual void HogeHoge_Implementation();

f:id:edo_m18:20180225214321p:plain

※2 PureとNon-Pureについて書かれている記事があったので、詳細はこちらをご覧ください→ [UE4] Pure関数とNonPure関数|株式会社ヒストリア

ドキュメント:UFUNCTION - Unreal Engine Wiki

Blueprintからプロパティに値を設定できるようにする

コンポーネントを作成し配置しても、適切にマクロを設定しておかないとBPからプロパティに対して値を設定することができません。

以下のように設定することでそれが可能となります。

UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn)

ドキュメント: UPROPERTY - Epic Wiki


その他

enumを定義する

l参考にしていたVideo Tutorialは若干古いバージョンだったため、その中で説明されていたenumだとエラーが出てコンパイルができませんでした。
ということで、いちおうメモ。

UENUM(BlueprintType)
enum class EHogeEnum : uint8
{
    EFuga,
    EFoo,
    EBar,
};

と、uint8を継承した形で宣言しないとなりません。

タイマーを利用する

タイマーの利用には、FTimerManagerクラスを利用します。
以下の例では、自身を再帰的にタイマーで呼び出します。

// header
FTimeHandle Timer;
void AAnyActor::TimerHandler();

// implementation
void AAnyActor::TimerHandler()
{
    // &AActor::GetWorldTimerManager
    float Delay = FMath::FRandRange(1.0f, 2.0f);
    GetWorldTimerManager().SetTimer(Timer, this, &AAnyActor::TimerHandler, Delay, false);
}

ドキュメントはこちら

docs.unrealengine.com


トラブルシューティング

TickComponentが呼ばれなくなった!

C++を書いていて、突然、少し前まで正常に動いていたTickComponentが動かなくなるケースがありました。

色々調べてみても、必要なフラグの扱いやらメソッドの定義やらは正常に行っている・・でも動かない。

最終的に解決したのは、「該当のComponentを一度消し、追加し直す」ことで解消しました。
そのあたりが書かれていた記事がこちら↓

community.gamedev.tv

まさか入れ直しだけで解決するとは・・。
おそらくですが、(社内のエンジンに詳しい人と話していて聞いたのは)Hot Reloadの機能がUE4には備わっていて、それの関連付けなどがおかしくなってしまったのでは、とのこと。
多分、その関連付け周りの処理が、コンポーネントの追加・削除のタイミングで行われているのでしょう。

なので、追加し直しで直ったのではないかなと。

ちなみに、TickComponentを呼ぶ必要があるかどうか、みたいなフラグ周りについては以下の記事が色々まとめてくれているので参考にしてみてください。

usagi.hatenablog.jp

case内の初期化

これはUEというよりC++の問題ですが、switch文内で初期化を伴う処理を書いている場合、case文を{}で囲まないとコンパイルエラーとなるようです。


その他Tips

色々な値をログ出力

Log Fomatting

  • LogMessage
UE_LOG(LogTemp, Log, TEXT("Hoge"));
  • Log an FString
FString anyString = ...;
UE_LOG(LogTemp, Log, TEXT("Log: %s"), *anyString);
  • Log an Bool
bool anyBool = ...;
UE_LOG(LogTemp, Log, TEXT(Bool value: %s"), anyBool ? TEXT("True") : TEXT("False")); 
  • Log an Int
int anyInt = ...;
UE_LOG(LogTemp, Log, TEXT("Int value: %d"), anyInt);
  • Log a Float
float anyFloat = ...;
UE_LOG(LogTemp, Log, TEXT("Float value: %f"), anyFloat);
  • Log an FVector
FVector anyVector = ...;
UE_LOG(LogTemp, Log, TEXT("FVector value: %s"), *anyVector.ToString());
  • Log an FName
FName anyName = ...;
UE_LOG(LogTemp, Log, TEXT("FName value: %s"), *anyName.ToString());

ドキュメントはこちら → Logs, Printing Messages To Yourself During Runtime

Tickメソッド外で時間を扱う

Tickメソッド内では、引数にDeltaTimeが渡ってくるのでそれを利用すればいいですが、それ以外の場合はワールドから取得する必要があります。

void AnyMethod()
{
    float time = GetWorld()->GetTimeSeconds();
    float deltaTime = GetWorld()->GetDeltaSeconds();

    UE_LOG(LogTemp, Log, TEXT("Time: %f, DeltaTime: %f"), time, deltaTime);
}

番外編

ちょっと昔に書いた記事ですが、Cocos2D-xでアプリを作る際にまとめた、C++関連の記事です。
UEとは関係ない部分もありますが、C++的なところは同じなので紹介。

qiita.com