Best Practices in Unreal Engine C++
Writing efficient, maintainable, and performant C++ code in Unreal Engine requires following best practices. Below is a categorized list of best practices, organized by theme and use case.
1. Memory Management & Smart Pointers
Use const&
to Avoid Unnecessary Copies
π Use Case: Passing large objects efficiently.
β
Best Practice: Use const Type&
for function parameters when an object doesnβt need to be modified.
void ProcessData(const FString& Data); // Avoids unnecessary copying
β Avoid:
void ProcessData(FString Data); // Unnecessary copy
Use TWeakObjectPtr<>
for Safe Object References
π Use Case: Referencing objects without preventing garbage collection.
β
Best Practice: Use TWeakObjectPtr<>
when holding a reference to a UObject
that might get deleted.
TWeakObjectPtr<AActor> TargetActor;
Before using, always check validity:
if (TargetActor.IsValid())
{
TargetActor->Destroy();
}
β Avoid: Keeping raw UObject*
pointers if the object can be destroyed.
Use TSoftObjectPtr<>
for Asset References
π Use Case: Referencing assets without forcing them to stay loaded.
β
Best Practice: Use TSoftObjectPtr<>
for assets to improve memory management.
TSoftObjectPtr<UTexture2D> Texture;
To load asynchronously:
UTexture2D* LoadedTexture = Texture.LoadSynchronous();
β Avoid: Using UObject*
for assets that should be loaded/unloaded dynamically.
2. UObject & Actor Lifecycle Management
Use CreateDefaultSubobject<>
for Component Creation
π Use Case: Creating components in the constructor.
β
Best Practice: Use CreateDefaultSubobject<>
to ensure proper ownership by the AActor
.
UStaticMeshComponent* MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
β Avoid: Using NewObject<>
inside a constructor, as it wonβt automatically register with the owning actor.
Use NewObject<>
for Dynamic Object Creation
π Use Case: Creating UObjects
at runtime.
β
Best Practice: Use NewObject<>
for transient objects that are not components.
UMyWidget* Widget = NewObject<UMyWidget>(this);
Use SpawnActor<>
for Runtime Actor Creation
π Use Case: Spawning actors at runtime.
β
Best Practice: Use SpawnActor<>
instead of new
to ensure proper memory management.
AEnemy* Enemy = GetWorld()->SpawnActor<AEnemy>(EnemyClass, SpawnLocation, SpawnRotation);
Always check validity:
if (Enemy)
{
Enemy->Init();
}
β Avoid: Using new AActor()
manuallyβit wonβt be properly managed by Unreal.
Use Destroy()
Instead of delete
for Actors
π Use Case: Properly removing actors from the game.
β
Best Practice: Use Destroy()
to safely remove an actor.
if (Enemy)
{
Enemy->Destroy();
}
β Avoid:
delete Enemy; // Can cause crashes
3. Performance Optimization
Mark Functions as const
When Possible
π Use Case: Indicating that a function does not modify the object state.
β
Best Practice:
FVector GetLocation() const;
Use FORCEINLINE
for Small, Performance-Critical Functions
π Use Case: Optimizing function calls.
β
Best Practice:
FORCEINLINE float GetHealth() const { return Health; }
Avoid Using Tick()
Unless Necessary
π Use Case: Reducing per-frame processing overhead.
β
Best Practice: Disable ticking when not needed.
PrimaryActorTick.bCanEverTick = false;
Use events instead of polling.
4. Asset & Data Management
Use FName
Instead of FString
When Possible
π Use Case: Optimizing string operations.
β
Best Practice: Use FName
for identifiers that donβt change often.
FName PlayerTag = TEXT("Player");
β Avoid:
FString PlayerTag = TEXT("Player"); // Slower, more memory usage
Use UPROPERTY()
to Manage Object References
π Use Case: Ensuring proper memory management for UObjects
.
β
Best Practice:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Components")
UStaticMeshComponent* MeshComponent;
Use UFUNCTION()
for Reflection & Blueprint Support
π Use Case: Exposing functions to Blueprints or delegates.
β
Best Practice:
UFUNCTION(BlueprintCallable, Category = "Gameplay")
void FireWeapon();
β Avoid: Using normal C++ functions when Unrealβs reflection system is needed.
5. Multithreading & Asynchronous Processing
Use AsyncTask
for Background Work
π Use Case: Running expensive tasks asynchronously.
β
Best Practice:
AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, []()
{
// Do work in the background
});
Use FGraphEventRef
for Dependency-Based Async Work
π Use Case: Scheduling tasks with dependencies.
β
Best Practice:
FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady([]()
{
// Async work
}, TStatId(), nullptr, ENamedThreads::GameThread);
6. Delegates & Event-Driven Programming
Use DECLARE_DYNAMIC_MULTICAST_DELEGATE
for Blueprint Events
π Use Case: Creating Blueprint-exposed event delegates.
β
Best Practice:
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnHealthChanged);
UPROPERTY(BlueprintAssignable, Category = "Events")
FOnHealthChanged OnHealthChanged;
β Avoid: Using C++-only delegates when Blueprints need access.
Use BindUFunction()
for Delegate Binding in Blueprints
π Use Case: Binding C++ functions dynamically.
β
Best Practice:
Button->OnClicked.AddDynamic(this, &UMyWidget::HandleButtonClick);
β Avoid: Using raw function pointers when working with UObject
delegates.
Conclusion
Following these best practices will improve performance, prevent memory leaks, and make your Unreal Engine C++ code more maintainable.
Would you like a specific example for any of these topics? π