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? πŸš€