目次
概要
まだまだ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との関連
なお、このPlayerController
とPlayerPawn
の関係は、PawnをコントロールするのがPlayerController
の役割です。
これらの設定はGameMode
に設定するようになっており、またGameMode
はプロジェクト設定にて設定され、これが実行時に起動するポイントとなるようです。
詳細はこちら↓
ワールドに存在するアクターをすべて取得する
#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
クラスにはGetActorLocation
とSetActorLocation
があるのでこれを利用する。
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のお作法に則って適切にセットアップする必要があります。
セットアップについては、以下の引越しガイドの「入力イベント」あたりに載っています。
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
から得ることができます。
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++で書いたクラス(コンポーネント)も、ブループリントから利用するように作成することが出来ます。
そのためのマクロがUPROPERTY
とUFUNCTION
です。
これらを適切に設定することで、ブループリントから設定できたり、あるいは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();
※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); }
ドキュメントはこちら
トラブルシューティング
TickComponentが呼ばれなくなった!
C++を書いていて、突然、少し前まで正常に動いていたTickComponent
が動かなくなるケースがありました。
色々調べてみても、必要なフラグの扱いやらメソッドの定義やらは正常に行っている・・でも動かない。
最終的に解決したのは、「該当のComponentを一度消し、追加し直す」ことで解消しました。
そのあたりが書かれていた記事がこちら↓
まさか入れ直しだけで解決するとは・・。
おそらくですが、(社内のエンジンに詳しい人と話していて聞いたのは)Hot Reloadの機能がUE4には備わっていて、それの関連付けなどがおかしくなってしまったのでは、とのこと。
多分、その関連付け周りの処理が、コンポーネントの追加・削除のタイミングで行われているのでしょう。
なので、追加し直しで直ったのではないかなと。
ちなみに、TickComponent
を呼ぶ必要があるかどうか、みたいなフラグ周りについては以下の記事が色々まとめてくれているので参考にしてみてください。
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++的なところは同じなので紹介。