Adding Custom Map Checks in UE4

Posted January 3rd, 2018 in game-development

The map check function in UE4 offers level designers an insight into things that are going wrong inside their levels - for example a static mesh actor with no mesh.

Two errors in the Unreal Engine 4 map checker output, against two actors which have null static meshes.

You can also harness this power in your own project to run anything in the game world through a custom set of rules specific to your content.

For example, in Estranged, all surfaces in all levels need to have a physical material assigned to them.

This assignment has various functions, but most importantly dictates the footstep sound when the player walks on the material, and the bullet decal and particle effect to spawn when the surface is shot.

Basic Syntax

To get this working, all that is required is to override the CheckForErrors() method in a custom actor. A very simple header looks like this:

UCLASS()
class ESTCORE_API AMyActor : public AActor
{
    GENERATED_BODY()

    UPROPERTY(Category = "Bad Things", EditAnywhere, BlueprintReadWrite)
    bool bBadThing;

#if WITH_EDITOR
    virtual void CheckForErrors() override;
#endif
};

An example implementation checking the state of that boolean property might look like this:

#include "Logging/TokenizedMessage.h"
#include "Logging/MessageLog.h"
#include "Misc/UObjectToken.h"
#include "Misc/MapErrors.h"
#include "MyActor.h"

#if WITH_EDITOR
void AMyActor::CheckForErrors()
{
    Super::CheckForErrors();

    FMessageLog MapCheck("MapCheck");

    if (bBadThing)
    {
        MapCheck.Warning()
            ->AddToken(FUObjectToken::Create(this))
            ->AddToken(FTextToken::Create(FText::FromString("has a bad thing!")));
    }
}
#endif

The output when running the map check in the editor looks like this:

The custom map checker error in the Unreal Engine 4 editor.

Complex Example

As I said above, Estranged needs a physical material assigned to each surface in the game to ensure things like footsteps and bullet impacts work properly. To achieve that, I use an empty actor which contains only a CheckForErrors() override as above.

This method is responsible for looping all actors in the world, and checking their materials for a physical material. This can be easily extended to check many other things, and this actor is dropped into every level in Estranged to ensure consistency across the entire game.

#if WITH_EDITOR
void AEstMapErrorChecker::CheckForErrors()
{
    Super::CheckForErrors();

    FMessageLog MapCheck("MapCheck");

    TSet<const UMaterialInterface*> SeenMaterials;
    
    for (TActorIterator<AActor> ActorItr(GetWorld()); ActorItr; ++ActorItr)
    {
        const AActor *Actor = *ActorItr;

        if (Actor->IsEditorOnly())
        {
            // Ignore editor only actors
            continue;
        }
        
        for (const UActorComponent* Component : Actor->GetComponents())
        {
            const UPrimitiveComponent* SceneComponent = Cast<UPrimitiveComponent>(Component);
            if (SceneComponent == nullptr)
            {
                // If there isn't a primitive component at this actor's root, ignore it
                continue;
            }

            if (SceneComponent->IsEditorOnly())
            {
                // Ensure the scene component is a game component
                continue;
            }

            if (!SceneComponent->GetCollisionEnabled())
            {
                // Physical materials not important for non-simulated components
                continue;
            }

            int32 NumMaterials = SceneComponent->GetNumMaterials();
            for (int32 i = 0; i < NumMaterials; i++)
            {
                const UMaterialInterface* Material = SceneComponent->GetMaterial(i);
                if (Material == nullptr)
                {
                    continue;
                }

                if (SeenMaterials.Contains(Material))
                {
                    continue;
                }

                if (Material->GetBlendMode() == EBlendMode::BLEND_Additive)
                {
                    // Additive materials aren't likely to be valid for footsteps/bullets
                    continue;
                }

                SeenMaterials.Add(Material);

                // If the physical material is null or default, log a warning
                if (Material->GetPhysicalMaterial() == nullptr || Material->GetPhysicalMaterial() == GEngine->DefaultPhysMaterial)
                {
                    MapCheck.Warning()
                        ->AddToken(FUObjectToken::Create(this))
                        ->AddToken(FTextToken::Create(FText::FromString("Actor")))
                        ->AddToken(FUObjectToken::Create(Actor))
                        ->AddToken(FTextToken::Create(FText::FromString("has material")))
                        ->AddToken(FUObjectToken::Create(Material))
                        ->AddToken(FTextToken::Create(FText::FromString("which does not have a valid physical material")));
                }
            }
        }
    }
}
#endif

This can be extended to check any piece of data in a level. If it is accessible from the world in code, it can be checked.

The performance of this actor should not be a big concern as the WITH_EDITOR guards mean it is excluded from a non-editor build, and the map check log provides an execution time counter for free:

Custom map checks in the editor, showing an execution timer.

Artists can then click on the highlighted words in the messages to navigate to the relevant content which needs fixing.

Tagged map level actor custom material game world estranged physical editor

Comments

Please click here to load comments.