From Blueprint to C++: A Migration Guide
When to keep logic in Blueprints, when to move to C++, and how to maintain a productive hybrid workflow.
When to Stay in Blueprint
Blueprint isn't just for beginners. Many production games ship with significant Blueprint code. Stay in Blueprint when:
- Rapid iteration matters: UI, prototypes, game modes
- Designers need access: Tunable gameplay parameters
- Performance isn't critical: Event-driven logic, one-time initialization
- Visual debugging helps: Complex state machines, AI behavior trees
When to Move to C++
Consider C++ when:
- Tight loops: Per-frame calculations over many objects
- Core systems: Networking, save systems, complex data structures
- Engine integration: Custom components, subsystems, editor tools
- Team scale: Large teams benefit from code review, typing, IDE features
- Reusability: Systems that will be used across multiple projects
Setting Up C++
Converting a Blueprint Project
- File → New C++ Class
- Choose a parent class (Actor, Character, etc.)
- Name your class
- UE generates files and opens your IDE
- Project is now a mixed Blueprint/C++ project
Project Structure
MyProject/
├── Source/
│ └── MyProject/
│ ├── MyProject.Build.cs
│ ├── MyProject.h
│ ├── MyProject.cpp
│ └── MyCharacter.cpp/h
├── Content/
│ └── Blueprints/
└── MyProject.uprojectThe Hybrid Approach
The best workflow uses both: C++ for the heavy lifting, Blueprint for the creative layer.
C++ Base, Blueprint Extension
// C++: Base character with core logic
UCLASS(Blueprintable)
class AMyCharacter : public ACharacter
{GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) float MaxHealth = 100.0f; UFUNCTION(BlueprintCallable) void TakeDamage(float Amount); UFUNCTION(BlueprintImplementableEvent) void OnDeath();};Designers create Blueprint children of the C++ class, overrideOnDeath to add effects, tweak MaxHealth per character type.
BlueprintNativeEvent Pattern
// C++ has default implementation
UFUNCTION(BlueprintNativeEvent)
void OnHit(float Damage);
// Implementation
void AMyCharacter::OnHit_Implementation(float Damage)
{Health -= Damage; // Blueprint can override to add effects}Migration Strategy
Step 1: Identify the Hot Path
Don't migrate everything. Use stat Blueprint to find what's actually expensive. Focus on:
- Functions called in Tick
- Loops over large arrays
- Functions called hundreds of times per frame
Step 2: Create C++ Interface
Write the C++ function with the same signature as your Blueprint function. Make it BlueprintCallable so the transition is seamless.
UFUNCTION(BlueprintCallable, Category="Combat")
float CalculateDamage(AActor* Target, float BaseDamage);Step 3: Call C++ from Blueprint
Replace the Blueprint implementation with a single node calling your C++ function. This lets you verify behavior before removing the Blueprint version entirely.
Step 4: Move More Logic
Once verified, move related logic to C++. Group related functions into classes (DamageCalculator, InventoryManager, etc.).
Common Patterns
Exposing C++ to Blueprint
// Callable from Blueprint
UFUNCTION(BlueprintCallable)
void DoThing();
// Readable/writable property
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Speed;
// Override in Blueprint
UFUNCTION(BlueprintNativeEvent)
void OnSpawn();
// Pure Blueprint implementation
UFUNCTION(BlueprintImplementableEvent)
void OnCustomEvent();Delegates for Events
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FOnHealthChanged, float, NewHealth);
UPROPERTY(BlueprintAssignable)
FOnHealthChanged OnHealthChanged;
// In C++
OnHealthChanged.Broadcast(Health);
// Blueprint binds to this eventData-Only Blueprints
Create C++ base classes with BlueprintReadWrite properties. Designers create Blueprint children that only set data—no graph logic. Best of both worlds: type safety + designer control.
Performance Comparison
| Operation | Blueprint | C++ | Speedup |
|---|---|---|---|
| Function call overhead | ~1μs | ~10ns | ~100x |
| Vector math (1000x) | ~0.5ms | ~0.01ms | ~50x |
| Array iteration (1000 elements) | ~0.3ms | ~0.01ms | ~30x |
| Simple branch logic | ~0.5μs | ~5ns | ~100x |
Raw speedup is significant, but remember: most code isn't in tight loops. A function that runs once per frame at 0.001ms doesn't benefit from being 100x faster.
Maintaining Both
Coding Standards
- C++ base classes go in
Source/ - Blueprint children go in
Content/Blueprints/ - Name Blueprint children with
BP_prefix - Document which functions are meant to be overridden
Testing
- Unit test C++ logic (Unreal Automation Framework)
- Functional test Blueprint behavior (in-editor playtests)
- Performance test both (stat commands, profiler)
Version Control
- C++ files: normal Git workflow
- Blueprint binaries: Git LFS
- Blueprint merge conflicts: often easier to redo than resolve
Common Mistakes
- Migrating too early: Profile first, optimize after
- Migrating everything: Keep designer-facing logic in Blueprint
- Poor interface design: Think about the Blueprint experience when designing C++ APIs
- Forgetting hot reload limits: Adding properties requires full rebuild
- Over-engineering: Start simple, refactor when needed
Summary
The Blueprint-to-C++ migration isn't binary. The most effective teams use both: C++ for systems and performance-critical code, Blueprint for rapid iteration and designer accessibility.
Start in Blueprint. Migrate when profiling shows you need to. Maintain clean interfaces between the two layers. This hybrid approach gives you the best of both worlds.