Skip to main content

12 / 16

Logging and comments

Log what failed and where. Comment the constraints the code cannot express.

A log line gets read months later by someone who cannot reproduce the bug, so it needs the identity and the value that failed. A comment has the same test: write down what the code cannot say for itself.

Rules

  1. 01Use a category per system so verbosity can change without recompiling.
  2. 02Include object identity, the rejected value, and the operation in a failure log.
  3. 03Use Error for an operation that failed, Warning for an actionable abnormal state, and lower verbosity for normal detail.
  4. 04Document caller obligations once, at the public declaration.
  5. 05Keep reflected types out of namespaces. Plain implementation helpers can live in a UE::Project::Private namespace.
  6. 06Use Display for a message that belongs on the console and in the log file. Log verbosity writes to the log file only, which is the right default for routine detail.
  7. 07Verbosity runs Fatal, Error, Warning, Display, Log, Verbose, VeryVerbose. Fatal always prints and crashes the process, even when logging is otherwise disabled.
  8. 08Use GetNameSafe() on any pointer that could be null. GetName() dereferences, so a diagnostic log becomes the thing that crashes the build you were diagnosing.
  9. 09A comment that restates the next line goes stale the moment that line changes. Write the constraint, the reason the obvious approach fails, or nothing.
  10. 10Do not let a warning fire every frame during normal play. It buries the warnings that actually need attention.

Include the details needed to reproduce a failure

A dedicated category plus the item, quantity, and actor makes this rejection searchable.

InventoryComponent.cppcpp
DEFINE_LOG_CATEGORY_STATIC(LogInventory, Log, All);

bool UInventoryComponent::TryAddItem(
    const UItemDefinition& Item,
    const int32 Quantity)
{
    if (Quantity <= 0)
    {
        UE_LOG(
            LogInventory,
            Warning,
            TEXT("Rejected %s with quantity %d on %s"),
            *Item.GetName(),
            Quantity,
            *GetNameSafe(GetOwner()));
        return false;
    }

    return InsertItem(Item, Quantity);
}
Why
You can enable LogInventory without turning every system up to Verbose. The message also records which value failed and where it happened.
Costs
Logs in frequent paths still cost formatting and storage when enabled. Use Verbose or VeryVerbose for detail and reserve warnings for actionable faults.
Instead
Use ensure for a recoverable invariant. Visual Logger is better for spatial gameplay problems, while Unreal Insights is built for timing.

Explain the reason the code cannot show

The function names already describe the operation. The comment explains why the removed ID must survive for one more tick.

InventoryComponent.cppcpp
void UInventoryComponent::RememberRemovalForLateRpc(const FGuid& ItemId)
{
    RecentlyRemovedItemIds.Add(ItemId);

    // A client retry can arrive after removal. Retain the ID through the next server tick.
    GetWorld()->GetTimerManager().SetTimerForNextTick(
        FTimerDelegate::CreateWeakLambda(this, [this, ItemId]()
        {
            RecentlyRemovedItemIds.Remove(ItemId);
        }));
}
Why
A later rewrite can preserve that network timing requirement even if it replaces the container or timer.
Costs
Comments go stale. Keep this one beside the constraint, and delete it if the code can make the reason clear on its own.
Instead
Document public caller obligations in the header. Use an assertion when the constraint can be checked instead of merely described.

Put private C++ helpers in a namespace

The helper stays out of global scope without wrapping an Unreal Header Tool type in a namespace.

TargetScoring.cppcpp
namespace UE::ActionGame::Private
{
    float ScoreTarget(const FVector& Origin, const AActor& Target)
    {
        const float DistanceSquared = FVector::DistSquared(
            Origin,
            Target.GetActorLocation());

        return 1.0f / FMath::Max(DistanceSquared, 1.0f);
    }
}

float UTargetingComponent::GetScore(const AActor& Target) const
{
    return UE::ActionGame::Private::ScoreTarget(
        GetOwner()->GetActorLocation(),
        Target);
}
Why
The helper cannot collide with a name from another module. Private also makes its intended scope clear.
Costs
Namespaced types are more verbose at call sites. Unreal Header Tool does not support wrapping UCLASS, USTRUCT, or reflected enums in namespaces.
Instead
Use a private class member when the helper depends on instance state. Use a named public UE::Project:: namespace for a deliberate plain C++ API.

All 16 rules