The Need for C++ Migration
Many people starting to learn Unreal Engine (UE) likely begin with Blueprint, which can be operated intuitively. Blueprint, a node-based visual scripting system, is a powerful tool for quickly building game logic without programming knowledge. However, as development progresses, haven't you faced challenges like these?
- Performance Limits: When performing complex calculations or large amounts of processing, Blueprint can become slow.
- Management Difficulty in Large Projects: Nodes become complexly tangled, reducing readability and maintainability.
- Deep Access to Engine Features: C++ is needed for low-level engine features and integration with external libraries.
To solve these challenges and aim for more advanced, optimized game development, migration to C++ is an inevitable path. C++ is the foundational language of UE, providing control and performance levels that Blueprint cannot achieve. This article targets intermediate developers with Blueprint experience, explaining concrete steps for smooth migration to C++ development and integration methods between the two.
Step 1: Understand Blueprint and C++ Role Division
When starting C++ development, you don't need to completely abandon Blueprint. The best practice for UE development is implementing foundational logic and performance-critical processing in C++, then using Blueprint to extend those C++ features or build settings and events that designers and level artists can easily handle.
| Feature | Blueprint (Visual Scripting) | C++ (Programming Language) |
|---|---|---|
| Execution Speed | Slower than C++ (executes as Blueprint bytecode) | Fast (compiles to native code) |
| Access Level | Limited to portion of engine features | Full access to entire engine, low-level features |
| Learning Curve | Gentle, intuitive | Steep, requires programming knowledge |
| Use Cases | Prototyping, UI logic, data settings, artist-facing events | Game core logic, complex calculations, performance-critical parts |
About Blueprint Execution Speed
Blueprint is indeed slower than C++, but for normal game logic (event handling, UI operations, simple calculations), there's almost no perceptible difference. Performance differences become problematic with complex processing executed every frame or handling large numbers of objects. Rather than "it's slow so migrate everything to C++," the efficient approach is to C++ify only the bottleneck portions.
Step 2: Creating C++ Classes and Basic Structure
To start C++ development in UE, first create a new C++ class from the editor.
1. Creating a C++ Class
From the editor's "File" > "New C++ Class...", select a parent class and create. Common parent classes include Actor, Pawn, Character, etc.
2. Basic C++ Code Structure
The created C++ files (e.g., MyCppActor.h and MyCppActor.cpp) contain UE-specific macros.
MyCppActor.h (Header File)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyCppActor.generated.h" // File where UE-specific macros are expanded
UCLASS() // Macro indicating this class is registered with UE's object system
class MYPROJECT_API AMyCppActor : public AActor
{
GENERATED_BODY() // Macro generating class boilerplate code
public:
// Constructor
AMyCppActor();
protected:
// Called when game starts
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Define function callable from Blueprint
UFUNCTION(BlueprintCallable, Category = "My Functions")
void PrintMessage(FString Message);
};
MyCppActor.cpp (Source File)
#include "MyCppActor.h"
AMyCppActor::AMyCppActor()
{
// Setting to call Tick function every frame
PrimaryActorTick.bCanEverTick = true;
}
void AMyCppActor::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Warning, TEXT("C++ Actor Started!"));
}
void AMyCppActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AMyCppActor::PrintMessage(FString Message)
{
// Log output
UE_LOG(LogTemp, Log, TEXT("Message from Blueprint: %s"), *Message);
}
Technical Terms: UE-Specific Macros
UCLASS()/USTRUCT()/UENUM(): Essential macros for registering classes, structs, and enums with UE's reflection system. These enable editor display, serialization (save/load), and Blueprint access.GENERATED_BODY(): Macro indicating where UE's build system inserts generated code (constructors, reflection information, etc.).UPROPERTY(): Registers member variables with reflection system, enabling editor editing and Blueprint access.UFUNCTION(): Registers member functions with reflection system, enabling Blueprint calls and timer registration.
Step 3: Integration with Blueprint (UPROPERTY and UFUNCTION)
Making C++-defined variables and functions usable from Blueprint is the most important step in migrating from Blueprint. Use UPROPERTY and UFUNCTION macros.
1. Blueprint-Accessible Variables (UPROPERTY)
Add specifiers like EditAnywhere or BlueprintReadOnly to UPROPERTY macro to control behavior in Blueprint and editor.
// MyCppActor.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Configuration")
float MovementSpeed = 500.0f; // Read/write from editor and Blueprint
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Status")
int CurrentHealth = 100; // Read-only from editor and Blueprint
2. Blueprint-Callable Functions (UFUNCTION)
Specifying BlueprintCallable in UFUNCTION macro makes the function callable as a Blueprint node.
// MyCppActor.h
UFUNCTION(BlueprintCallable, Category = "Combat")
void ApplyDamageToActor(float DamageAmount);
// Note: TakeDamage name already exists in AActor,
// so use a different name for custom functions or implement as override
Implementation Example: Damage Processing
// MyCppActor.cpp
void AMyCppActor::ApplyDamageToActor(float DamageAmount)
{
CurrentHealth -= FMath::RoundToInt(DamageAmount);
UE_LOG(LogTemp, Log, TEXT("Took %f damage. Health remaining: %d"), DamageAmount, CurrentHealth);
if (CurrentHealth <= 0)
{
// Handle complex death logic in C++...
UE_LOG(LogTemp, Warning, TEXT("Actor Died!"));
}
}
This ApplyDamageToActor function can be called as a node from Blueprint's event graph, executing performance-critical damage calculation on the C++ side while making results available to Blueprint.
Common Mistakes and Best Practices
Common Mistakes
- Implementing same processing in both Blueprint and C++: Concentrate logic on one side, let the other focus on calls and settings.
- Writing too much Blueprint logic in C++ header files: C++ header files (
.h) should define only minimal interface needed for Blueprint access (UPROPERTY,UFUNCTION), concentrate implementation in (.cpp). - Fearing compile errors: C++ requires compilation so errors appear more than Blueprint, but reading error messages deepens understanding of UE's internal structure.
Best Practices
- Position C++ as "foundation," Blueprint as "extension": Create performance-critical core systems and highly reusable components in C++, inherit/extend those to implement specific gameplay logic in Blueprint.
- Use
BlueprintImplementableEvent: Use when defining events in C++ where "I want to delegate processing to Blueprint at this timing."
// MyCppActor.h
UFUNCTION(BlueprintImplementableEvent, Category = "Events")
void OnDeath(); // Called in C++, implemented in Blueprint
// MyCppActor.cpp
void AMyCppActor::TakeDamage(float DamageAmount)
{
// ... (damage processing)
if (CurrentHealth <= 0)
{
OnDeath(); // Call Blueprint-implemented event
}
}
Key Points for C++ Migration
Migrating from Blueprint to C++ is an important step for elevating your skills as an Unreal Engine developer.
- Understand Role Division: C++ for performance and foundation, Blueprint for flexible extension and settings.
- Master Basic Structure: Understand UE-specific macros like
UCLASS,GENERATED_BODY,UPROPERTY,UFUNCTION. - Establish Integration: Master
BlueprintCallableandBlueprintImplementableEventto maximize the strengths of both C++ and Blueprint.
It may feel difficult at first, but mastering C++ will allow your Unreal Engine project to evolve larger and higher-performance. We recommend starting with simple Actor classes, gradually expanding scope.