【Unreal Engine】BlueprintからC++への第一歩:最初のクラスと3つのマクロ

作成: 2025-12-12最終更新: 2026-07-20

Blueprintしか触ってこなかった人向けに、最初のC++クラスの作り方を図解。UCLASS・UPROPERTY・UFUNCTIONでC++の変数と関数をBlueprintへ公開する手順、Live Codingの制限、C++で親・BPで子を作る実践まで。

Blueprintでひと通りゲームが動くようになると、C++という選択肢が視界に入ってきます。ただ、入門記事を開くと UCLASS()GENERATED_BODY() が並んでいて、どこから手をつけるものなのかが見えません。

UEのC++は、Blueprintを捨てて全部書き直すためのものではありません。土台だけをC++で作り、その上はこれまで通りBlueprintで組む、という形が一般的です。この記事では、最初のC++クラスを作る手順と、UCLASS / UPROPERTY / UFUNCTION の3つのマクロでC++の変数・関数をBlueprintへ公開する方法を解説します。

C++の土台ブロックの上にBlueprintのノードが乗っている図。C++は土台、Blueprintは組み立て

この記事でわかること

  • C++を使う理由は速度より 「Blueprintでは書きづらいものがある」 こと
  • Tools > New C++ Class... から最初のクラスを作る手順
  • 3つのマクロ の役割: UCLASS(登録)/ UPROPERTY(変数の公開)/ UFUNCTION(関数の公開)
  • EditAnywhere と BlueprintReadWrite は別の役割 という指定子の読み方
  • Live Coding で反映できる変更と、できない変更
  • 実践: C++で親クラスを作り、Blueprintで子を作って値を調整する

Sponsored

なぜC++を使うのか

「C++は速いから」という説明をよく見かけますが、個人開発で最初にC++が欲しくなる場面は、速度とは別のところにあります。 Blueprintのままだと素直に書けないもの が出てくるからです。

困ることBlueprintでの状況
入れ子のデータ構造変数の型に「配列の配列」や「Mapの値に配列」を直接作れない
差分を見る・マージするBlueprintはバイナリのアセット。Gitで中身の差分が読めず、競合すると手作業で直せない
まとめて直す「この関数名だけ全プロジェクトから探す」ができない。テキスト検索や一括置換が効かない
共通の土台を配る20個のBPに同じ変数を足すには、20回同じ作業をする

速度の話が効いてくるのは、 毎フレーム大量に回る処理 に限られます。ボタンを押したら扉が開く、といったイベント駆動の処理をC++に移しても体感は変わりません。ボトルネックの探し方はStatコマンドの記事で扱っています。

左:BPを20個複製して同じ変数を足す作業。右:C++の親クラスに1回書けば20個の子に届く

逆に言えば、上の表に当てはまらないうちは、Blueprintのままで問題ありません。「共通の土台を配りたくなったとき」が、最初のC++クラスを作るタイミングです。


最初のC++クラスを作る

エディタのメニューから Tools > New C++ Class... を選びます。ウィザードが開くので、順に決めていきます。

手順1:親クラスを選ぶ

よく使う親クラスがボタンで並んでいます。用途で選んでください。

親クラス何になるか
Actorレベルに配置・スポーンできるもの(アイテム、扉、ギミック)
Character歩く・ジャンプするキャラクター
Actor ComponentActorに付けて機能を足す部品(→ Componentの記事
Noneどれにも当てはまらない素のオブジェクト

手順2:名前と置き場所を決める

名前を入力します。UEには クラス名の頭文字のルール があります。

  • A: Actorの派生(AItemPickup
  • U: Actor以外のUObject派生(UMyComponent
  • F: 構造体などの素のC++型(FEnemyStats

ウィザードで ItemPickup と打つと、AItemPickup として作られます。Public / Private の選択は、迷ったら Public で構いません。

補足: Blueprintだけで作ってきたプロジェクトに初めてC++クラスを追加すると、そのプロジェクトはC++プロジェクトになります。ビルド環境(WindowsならVisual Studioと「C++によるゲーム開発」ワークロード)が必要です。作成後にエディタの再起動を求められることがあります。

手順3:生成されたファイルを見る

ItemPickup.hItemPickup.cpp の2つができます。ヘッダー(.h)が「何を持っているかの宣言」、ソース(.cpp)が「実際の処理」です。

// ItemPickup.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemPickup.generated.h"   // 必ず最後のinclude

UCLASS()
class MYPROJECT_API AItemPickup : public AActor
{
    GENERATED_BODY()

public:
    AItemPickup();

protected:
    virtual void BeginPlay() override;

public:
    virtual void Tick(float DeltaTime) override;
};

初見で気になる部分だけ拾っておきます。

  • #include "ItemPickup.generated.h"必ずincludeの最後 に置きます。順番を変えるとビルドが通りません
  • MYPROJECT_API はプロジェクト名から作られる文字列で、他のモジュールから使えるようにする印です。触る必要はありません
  • GENERATED_BODY() は、UEが裏で生成するコードの差し込み口です。これも消さずに置いておきます
Sponsored

3つのマクロでBlueprintに橋を架ける

C++で書いただけでは、Blueprintからは何も見えません。 見せたいものにマクロで印を付ける 必要があります。この印を読み取る仕組みを リフレクションシステム と呼びます。

C++の箱からBlueprint側へ3本の橋。UCLASSがクラス、UPROPERTYが変数、UFUNCTIONが関数を渡している
マクロ付ける対象付けると何ができるか
UCLASS()クラスこのクラスをBlueprintの親にできる。エディタが認識する
UPROPERTY()メンバ変数詳細パネルでの編集、Blueprintでの読み書き、セーブ対象になる
UFUNCTION()メンバ関数Blueprintのノードとして呼べる。タイマーやデリゲートに登録できる

印の付いていない変数には、もう1つ落とし穴があります。 UPROPERTY を付けないUObjectのポインタは、ガベージコレクションの対象から守られません 。参照しているつもりのオブジェクトが回収され、クラッシュにつながります。UObject系のメンバ変数には、Blueprintに出す予定がなくても UPROPERTY() を付けておくのが作法です。

UPROPERTY()
TObjectPtr<UStaticMeshComponent> MeshComponent;   // GCから守られる

指定子の読み方:EditAnywhereとBlueprintReadWriteは別物

UPROPERTY のカッコの中に書くものを 指定子(Specifier) と呼びます。ここが最初のつまずきどころです。 EditAnywhereBlueprintReadWrite は、別々のドアを開けています

  • Edit系: 詳細パネル(Details)で 値を編集 できるか
  • BlueprintRead系: Blueprintのグラフで Get / Set ノード を使えるか
左の扉に「詳細パネルで編集」=Edit系、右の扉に「グラフでGet/Set」=BlueprintRead系。別々の扉であることを示す図

Edit系(詳細パネル側)

指定子どこで編集できるか
EditAnywhereBlueprintのクラスデフォルトでも、レベルに置いた個体でも編集できる
EditDefaultsOnlyBlueprintのクラスデフォルトのみ。個体ごとには変えられない
EditInstanceOnlyレベルに置いた個体のみ。クラス側では変えられない
VisibleAnywhere表示されるが編集できない(デバッグ表示向き)

BlueprintRead系(グラフ側)

指定子グラフでできること
BlueprintReadWriteGetノードも Setノードも使える
BlueprintReadOnlyGetノードのみ。BP側から書き換えられない
(書かない)グラフに一切出てこない

組み合わせの実例です。

// 設計者が調整する値。個体ごとに変えたいので EditAnywhere、
// 実行中にBPから書き換える必要はないので BlueprintReadOnly
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Pickup")
float HealAmount = 25.0f;

// 実行中に変わる状態。エディタでは見るだけ、BPからは読み書きしたい
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Runtime")
bool bIsConsumed = false;

Category はグループ名です。詳細パネルで折りたためる見出しになるので、変数が増えてきたら必ず付けておきます。

UFUNCTION側の指定子

関数側は、まず2つ覚えれば足ります。

指定子ノードの見た目
BlueprintCallable白い実行ピンのあるノード。処理として呼ぶ
BlueprintPure実行ピンのない緑のノード。値を返すだけ(const にする)
UFUNCTION(BlueprintCallable, Category = "Pickup")
void Consume(AActor* Taker);

UFUNCTION(BlueprintPure, Category = "Pickup")
float GetRemainingHeal() const;

もう1つ、C++からBlueprintを 呼ぶ 側の指定子もあります。BlueprintImplementableEvent を付けた関数は、C++では宣言だけして中身を書かず、実装をBlueprintのイベントとして書きます。「拾われた瞬間の演出はBP側で自由にやってほしい」という橋渡しに使います。

// ItemPickup.h — C++は「呼ぶタイミング」だけを決める
UFUNCTION(BlueprintImplementableEvent, Category = "Pickup")
void OnPickedUpEffect();
Sponsored

ビルドとLive Codingの現実

C++を書いたら、Blueprintのコンパイルボタンとは別に ビルド が必要です。UE5には Live Coding が標準で有効になっており、エディタを開いたまま Ctrl + Alt + F11 でコードを再ビルドして反映できます。

ただし、Live Codingには 反映できる変更とできない変更 があります。ここを知らないと「書き換えたのに反映されない」で時間を溶かします。

Live Codingで通るのは関数の中身の変更。ヘッダーに手を入れたらエディタを閉じてビルドし直す、という分岐図
変更内容Live Codingで反映できるか
.cpp の関数の中身を書き換えるできる
ログの追加、計算式の修正できる
変数を追加する / UPROPERTY を書き換えるできない。エディタを閉じてビルドし直す
クラスを追加する / 親クラスを変えるできない。同上

判断はシンプルで、 ヘッダー(.h)に手を入れたらエディタを閉じる と覚えておけば大きく外しません。エディタを閉じた状態でIDEからビルドし、終わってからエディタを開き直します。

補足: 「ビルドは通ったのに詳細パネルに変数が出てこない」ときは、たいていLive Codingでヘッダーの変更を通そうとしています。一度エディタを閉じて、フルビルドしてください。


実践:C++で親を作り、Blueprintで子を量産する

ここまでの3つのマクロを、実際の形にします。作るのは 拾うと効果が出るアイテム です。FPSの回復パック、ローグライクの床落ちアイテム、レースゲームのブーストパネル。「近づくと消えて、何かが起きる」という構図は、ジャンルを問わず同じです。

C++には「回転する」「重なったら消える」という 共通の骨組み だけを書き、回復量や回転速度、拾ったときの演出は Blueprintの子クラス で決めます。

完成形

C++クラスAItemPickupから、BP_HealthPotion・BP_AmmoBox・BP_SpeedBoostの3つの子BPが伸び、それぞれ数値だけが違う図

再現条件

AItemPickup(親クラス:Actor)に、次のメンバを用意します。

メンバ名初期値指定子
MeshComponentUStaticMeshComponent*なしVisibleAnywhere
CollisionSphereUSphereComponent*半径 100.0VisibleAnywhere
RotationSpeedfloat90.0fEditAnywhere, BlueprintReadOnly
HealAmountfloat25.0fEditAnywhere, BlueprintReadOnly
OnPickedUpEffect()関数(引数なし・戻り値なし)BlueprintImplementableEvent

C++側:骨組みを書く

// ItemPickup.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemPickup.generated.h"

class USphereComponent;

UCLASS()
class MYPROJECT_API AItemPickup : public AActor
{
    GENERATED_BODY()

public:
    AItemPickup();

    virtual void Tick(float DeltaTime) override;

protected:
    virtual void BeginPlay() override;

    // 見た目。BP側でメッシュを差し替える
    UPROPERTY(VisibleAnywhere, Category = "Components")
    TObjectPtr<UStaticMeshComponent> MeshComponent;

    // 拾う判定の範囲
    UPROPERTY(VisibleAnywhere, Category = "Components")
    TObjectPtr<USphereComponent> CollisionSphere;

    // 1秒あたりの回転角度。子BPで調整する
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Pickup")
    float RotationSpeed = 90.0f;

    // 拾ったときの回復量。子BPで調整する
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Pickup")
    float HealAmount = 25.0f;

    // 拾った瞬間の演出。実装はBlueprint側に任せる
    UFUNCTION(BlueprintImplementableEvent, Category = "Pickup")
    void OnPickedUpEffect();

    UFUNCTION()
    void OnSphereOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
                         UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
                         bool bFromSweep, const FHitResult& SweepResult);
};
// ItemPickup.cpp
#include "ItemPickup.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/Character.h"

AItemPickup::AItemPickup()
{
    PrimaryActorTick.bCanEverTick = true;

    CollisionSphere = CreateDefaultSubobject<USphereComponent>(TEXT("CollisionSphere"));
    CollisionSphere->InitSphereRadius(100.0f);
    RootComponent = CollisionSphere;

    MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
    MeshComponent->SetupAttachment(RootComponent);
    // 判定はSphereに任せるので、メッシュ側は当たらないようにする
    MeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

void AItemPickup::BeginPlay()
{
    Super::BeginPlay();

    CollisionSphere->OnComponentBeginOverlap.AddDynamic(this, &AItemPickup::OnSphereOverlap);
}

void AItemPickup::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // 1秒間に RotationSpeed 度ずつ回す
    AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
}

void AItemPickup::OnSphereOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor,
                                  UPrimitiveComponent* OtherComp, int32 OtherBodyIndex,
                                  bool bFromSweep, const FHitResult& SweepResult)
{
    // プレイヤー(Character)以外は無視する
    if (!OtherActor->IsA(ACharacter::StaticClass()))
    {
        return;
    }

    UE_LOG(LogTemp, Log, TEXT("%s picked up. Heal: %.1f"), *GetName(), HealAmount);

    // 演出はBlueprint側の実装を呼ぶ
    OnPickedUpEffect();

    Destroy();
}

Blueprint側:子クラスを作って値を入れる

コンテンツブラウザの C++ Classes フォルダで ItemPickup を右クリックし、 Create Blueprint class based on ItemPickup を選びます。名前を BP_HealthPotion にしてください。

開くと、詳細パネルに Pickup カテゴリができています。ここで値を入れます。

子BPRotationSpeedHealAmountMesh
BP_HealthPotion90.025.0ポーションのメッシュ
BP_AmmoBox0.00.0箱のメッシュ
BP_SpeedBoost360.00.0パネルのメッシュ

イベントグラフでは、 右クリック > Event On Picked Up Effect を探して置きます。C++で BlueprintImplementableEvent にした関数が、赤いイベントノードとして出てきます。ここに Spawn System at Location(Niagara)と Play Sound at Location を繋げば、演出だけをBP側で自由に組めます。

BP_HealthPotion(子クラス)
Event On Picked Up Effect
  → Spawn System at Location(System: NS_Heal, Location: Get Actor Location)
  → Play Sound at Location(Sound: SFX_Heal, Location: Get Actor Location)

確認する

3つの子BPをレベルに置いてPlayします。

  • BP_HealthPotion4秒でちょうど1回転 します(90度/秒 × 4秒 = 360度)
  • BP_SpeedBoost1秒で1回転 します(360度/秒)
  • BP_AmmoBoxRotationSpeed が0なので 止まったまま です
  • プレイヤーで触れるとアクターが消え、Output Logに BP_HealthPotion_C_0 picked up. Heal: 25.0 が出ます

回転しない場合は、PrimaryActorTick.bCanEverTick = true; がコンストラクタにあるかを確認してください。触れても消えない場合は、CollisionSphere の Collision Presets が OverlapAllDynamic になっているか、プレイヤーのカプセルが Overlap を返す設定かを見ます(→ コリジョンプリセットの記事)。

ポイントは2つです。

  • C++側は「変わらない手順」だけを持つ: 回して、触れたら消して、演出を呼ぶ。この骨組みは全アイテム共通です。4種類目を足す作業は、子BPを1つ作って数値を入れるだけになります
  • BlueprintImplementableEvent で演出を明け渡す: C++が「いつ呼ぶか」を決め、Blueprintが「何をするか」を決めます。エフェクトの差し替えのたびにビルドを待つ必要がなくなり、Event Dispatcherと同じ疎結合が、C++とBPの間にも作れます
Sponsored

おまけ:先に知っておくと良いこと

  • .generated.h は最後のinclude: 順番を変えるとビルドエラーになります。エラーメッセージが .generated.h を指していたら、まずinclude順を疑ってください
  • ヘッダーでは前方宣言を使う: class USphereComponent; のように名前だけ宣言しておき、実体のincludeは .cpp で行います。ヘッダーのincludeが増えるほどビルドが遅くなるためです
  • Blueprint側の変更はC++に戻せない: 子BPで足した変数はC++からは見えません。あとから親に上げたくなったら、C++に書き直して子BPの変数を消す作業が必要です。「共通になりそうなもの」は最初からC++に置いておくと楽です
  • 既存のBlueprintの親をC++に差し替えられる: Blueprintを開いて File > Reparent Blueprint から、作ったC++クラスを選べます。ただし変数名が衝突すると値が飛ぶことがあるので、作業前にバックアップを取ってください
  • UDataAsset 派生の型定義から始めるのも手: Actorをいきなり書くより、データの型だけをC++で定義する方が影響範囲が小さく、失敗しても戻しやすいです(→ Data Assetの記事

まとめ

  • C++を使う理由は速度より Blueprintでは書きづらいものがある こと。差分・一括修正・共通の土台がその代表
  • 作成は Tools > New C++ Class... から。親クラスを選び、A / U の命名ルールに沿って名前を付ける
  • Blueprintへの橋は 3つのマクロUCLASS(クラス)/ UPROPERTY(変数)/ UFUNCTION(関数)
  • 指定子は Edit系=詳細パネル、BlueprintRead系=グラフ 。別々のドアなので、両方欲しければ両方書く
  • ヘッダーを触ったらエディタを閉じてビルド 。Live Codingが通るのは関数の中身までと覚える
  • 基本形は C++で親、Blueprintで子 。骨組みをC++に、調整と演出をBPに置く

どこまでをC++に持たせ、どこからをBlueprintに残すか。その線引きはBlueprintとC++の役割分担で詳しく扱います。

いまのプロジェクトで、同じ変数を何度もコピーして回っているBlueprintはありませんか。その共通部分が、最初のC++クラスの中身になります。