How to create destructible objects in Unreal Engine 4 and Blender





Modern games are becoming more realistic, and one way to achieve this is to create destructible environments. Plus, smashing furniture, plants, walls, buildings, and entire cities is just fun.



The most striking examples of games with good destructibility are Red Fraction: Guerrilla, with its ability to tunnel through Mars, Battlefield: Bad Company 2, where you can turn the entire server into ashes if you want, and Control with its procedural destruction of everything that catches your eye.



In 2019, Epic Games unveiled a demo of Unreal's new high-performance physics and destruction system, Chaos . The new system allows you to create destruction of different scales, has support for the Niagara effect editor, and at the same time is distinguished by an economical use of resources.



In the meantime, Chaos is in beta testing, let's talk about alternative approaches to creating destructible objects in Unreal Engine 4. In this article, we will describe one of them in detail.





Requirements



Let's start by listing what we would like to achieve:



  • Artistic control. We want our artists to be able to create destructible objects as they please.
  • Destruction that does not affect gameplay. They should be purely visual, not disturbing anything related to the gameplay.
  • Optimization. We want to have complete control over performance and not let the CPU go down.
  • Easy to install. Setting up the configuration of such objects should be understandable to artists, therefore it is necessary that it includes only the necessary minimum of steps.


The destructible environments from Dark Souls 3 and Bloodborne were taken as a reference in this article.



image



main idea



In fact, the idea is simple:



  • Create a visible baseline mesh
  • Add hidden parts of the mesh;
  • On destruction: hide the base mesh -> show its parts -> start physics.


image



image



Preparing assets



We will use Blender to prepare objects. To create a mesh along which they will collapse, we use a Blender add-on called Cell Fracture.



Enabling the addon



First we need to enable the addon as it is disabled by default. Enabling Cell Fracture addon



image





Search addon (F3)



Then enable the addon on the selected grid.



image



Configuration settings



image



Addon launch



Watch the video, check the settings from there. Make sure you set up your materials correctly.





Material selection for unfolding cut pieces



Then we will create a UV map for these parts.



image



image



Adding Edge Split



Edge Split will fix the shading.



image



Link modifiers



Using them will apply Edge Split to all selected parts.



image



Completion



This is how it looks in Blender. Basically, we don't need to model all the parts separately.



image



Implementation



Base class



Our destructible object is an Actor, which has several components:



  • Root scene;
  • Static Mesh - base mesh;
  • Collision box;
  • Floor box;
  • Radial force.


image



Let's change some settings in the constructor:



  • Disable the Tick timer feature (never forget to disable it for actors who don't need it);
  • We set up static mobility for all components;
  • Disable the influence on navigation;
  • Configuring collision profiles.


Setting up an actor in the constructor
ADestroyable::ADestroyable()
{
    PrimaryActorTick.bCanEverTick = false; // Tick
    bDestroyed = false; 

    RootScene = CreateDefaultSubobject<USceneComponent>(TEXT("RootComp")); //  ,   
    RootScene->SetMobility(EComponentMobility::Static);
    RootComponent = RootScene;

    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("BaseMeshComp")); //  
    Mesh->SetMobility(EComponentMobility::Static);
    Mesh->SetupAttachment(RootScene);

    Collision = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionComp")); // ,    
    Collision->SetMobility(EComponentMobility::Static);
    Collision->SetupAttachment(Mesh);

    OverlapWithNearDestroyable = CreateDefaultSubobject<UBoxComponent>(TEXT("OverlapWithNearDestroyableComp")); // ,    
    OverlapWithNearDestroyable->SetMobility(EComponentMobility::Static);
    OverlapWithNearDestroyable->SetupAttachment(Mesh);

    Force = CreateDefaultSubobject<URadialForceComponent>(TEXT("RadialForceComp")); //       
    Force->SetMobility(EComponentMobility::Static);
    Force->SetupAttachment(RootScene);
    Force->Radius = 100.f;
    Force->bImpulseVelChange = true;
    Force->AddCollisionChannelToAffect(ECC_WorldDynamic);

    /*   */
    Mesh->SetCollisionObjectType(ECC_WorldDynamic);
    Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    Mesh->SetCollisionResponseToAllChannels(ECR_Block);
    Mesh->SetCollisionResponseToChannel(ECC_Visibility, ECR_Ignore);
    Mesh->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
    Mesh->SetCollisionResponseToChannel(ECC_CameraFadeOverlap, ECR_Overlap);
    Mesh->SetCollisionResponseToChannel(ECC_Interaction, ECR_Ignore);
    Mesh->SetCanEverAffectNavigation(false);

    Collision->SetBoxExtent(FVector(50.f, 50.f, 50.f));
    Collision->SetCollisionObjectType(ECC_WorldDynamic);
    Collision->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
    Collision->SetCollisionResponseToAllChannels(ECR_Ignore);
    Collision->SetCollisionResponseToChannel(ECC_Melee, ECR_Overlap);
    Collision->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
    Collision->SetCollisionResponseToChannel(ECC_Projectile, ECR_Overlap);
    Collision->SetCanEverAffectNavigation(false); 

    Collision->OnComponentBeginOverlap.AddDynamic(this, &ADestroyable::OnBeginOverlap);
    Collision->OnComponentEndOverlap.AddDynamic(this, &ADestroyable::OnEndOverlap);

    OverlapWithNearDestroyable->SetBoxExtent(FVector(40.f, 40.f, 40.f));
    OverlapWithNearDestroyable->SetCollisionObjectType(ECC_WorldDynamic);
    OverlapWithNearDestroyable->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  ,       
    OverlapWithNearDestroyable->SetCollisionResponseToAllChannels(ECR_Ignore);
    OverlapWithNearDestroyable->SetCollisionResponseToChannel(ECC_WorldDynamic, ECR_Overlap);
    OverlapWithNearDestroyable->CanCharacterStepUp(false);
    OverlapWithNearDestroyable->SetCanEverAffectNavigation(false); 
}




In Begin Play, we collect some data and customize it:



  • We are looking for all parts with the "dest" tag;
  • Set up collisions for all parts so that the artist doesn't have to think about it;
  • Establish static mobility;
  • Hide all parts.


Setting up parts of an object in Begin Play
void ADestroyable::ConfigureBreakablesOnStart()
{
    Mesh->SetCullDistance(BaseMeshMaxDrawDistance); //       

    for (UStaticMeshComponent* Comp : GetBreakableComponents()) //    
    {
        Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  
        Comp->SetCollisionResponseToAllChannels(ECR_Ignore); //  
        Comp->SetCollisionResponseToChannel(ECC_WorldStatic, ECR_Block);
        Comp->SetMobility(EComponentMobility::Static); //     ,   
        Comp->SetHiddenInGame(true); //    ,        
    }
}




Simple function to get component parts
TArray<UStaticMeshComponent*> ADestroyable::GetBreakableComponents()
{
    if (BreakableComponents.Num() == 0) //     -  ?
    {
        TInlineComponentArray<UStaticMeshComponent*> ComponentsByClass; //    
        GetComponents(ComponentsByClass);

        TArray<UStaticMeshComponent*> ComponentsByTag; //      Β«destΒ»
        ComponentsByTag.Reserve(ComponentsByClass.Num());
        for (UStaticMeshComponent* Component : ComponentsByClass)
        {
            if (Component->ComponentHasTag(TEXT("dest")))
            {
                ComponentsByTag.Push(Component);
            }
        }
        BreakableComponents = ComponentsByTag; //     
    }
    return BreakableComponents;
}




Destruction triggers



There are three ways to provoke destruction.



OnOverlap



Destruction occurs when someone throws or otherwise uses an object that activates the process, such as a rolling ball.



image



OnTakeDamage The



object being destroyed takes damage.



image



OnOverlapWithNearDestroyable



In this case, one object being destructible overlaps another. In our case, for simplicity, they both break.



image



Object destruction flow





image

Object destruction diagram



Show destructible parts
void ADestroyable::ShowBreakables(FVector DealerLocation, bool ByOtherDestroyable /*= false*/)
{
    float ImpulseStrength = ByOtherDestroyable ? -500.f : -1000.f; //   
    FVector Impulse = (DealerLocation - GetActorLocation()).GetSafeNormal() * ImpulseStrength; //        ,        
for (UStaticMeshComponent* Comp : GetBreakableComponents()) //    
    {
        Comp->SetMobility(EComponentMobility::Movable); // 
        FBodyInstance* RootBI = Comp->GetBodyInstance(NAME_None, false);
        if (RootBI)
        {
            RootBI->bGenerateWakeEvents = true; //     

            if (PartsGenerateHitEvent)
            {
                RootBI->bNotifyRigidBodyCollision = true; //   OnComponentHit
                Comp->OnComponentHit.AddDynamic(this, &ADestroyable::OnPartHitCallback); //        
            }
        }

        Comp->SetHiddenInGame(false); //    
        Comp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); //  
        Comp->SetSimulatePhysics(true); //  
        Comp->AddImpulse(Impulse, NAME_None, true); //   

        if (ByOtherDestroyable)
            Comp->AddAngularImpulseInRadians(Impulse * 5.f); //       ,   

        //     
        Comp->SetCullDistance(PartsMaxDrawDistance);

        Comp->OnComponentSleep.AddDynamic(this, &ADestroyable::OnPartPutToSleep); //      
    }
}




The main function of destruction
void ADestroyable::Break(AActor* InBreakingActor, bool ByOtherDestroyable /*= false*/)
{
    if (bDestroyed) //   ,     
        return;

    bDestroyed = true;
    Mesh->SetHiddenInGame(true); //   
    Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); //     
    Collision->SetCollisionEnabled(ECollisionEnabled::NoCollision); //     
    OverlapWithNearDestroyable->SetCollisionEnabled(ECollisionEnabled::NoCollision); 
    ShowBreakables(InBreakingActor->GetActorLocation(), ByOtherDestroyable); // show parts 
    Force->bImpulseVelChange = !ByOtherDestroyable; //   ,     
    Force->FireImpulse(); //   

    /*     */
    OverlapWithNearDestroyable->SetCollisionEnabled(ECollisionEnabled::QueryOnly); //      
    TArray<AActor*> OtherOverlapingDestroyables;
    OverlapWithNearDestroyable->GetOverlappingActors(OtherOverlapingDestroyables, ADestroyable::StaticClass()); //     
    for (AActor* OtherActor : OtherOverlapingDestroyables)
    {
        if (OtherActor == this)
            continue;

        if (ADestroyable* OtherDest = Cast<ADestroyable>(OtherActor))
        {
            if (OtherDest->IsDestroyed()) // ,    
                continue;

            OtherDest->Break(this, true); //   
        }
    }

    OverlapWithNearDestroyable->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  

    GetWorld()->GetTimerManager().SetTimer(ForceSleepTimerHandle, this, &ADestroyable::ForceSleep, FORCESLEEPDELAY, false); //    ,       
    
    if(bDestroyAfterDelay)
        GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false); //    ,     

    OnBreakBP(InBreakingActor, ByOtherDestroyable); // blueprint    
}




What to do with sleep function



When the Sleep function is triggered, we disable physics / collisions and set static mobility. This will increase productivity.



Every primitive component with physics can go to sleep. We bind to this function on destruction.



This function can be inherent in any primitive. We bind to it to complete the action on the object.



Sometimes the physical object does not go to sleep and continues to update, even if you do not see any movement. If it continues to simulate physics, we make all of its parts go to sleep after 15 seconds.



Forced sleep function called by timer
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
    InComp->SetSimulatePhysics(false); //   
    InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  
    InComp->SetMobility(EComponentMobility::Static); //      
    /*         */
}




What to do with destruction



We need to check if the actor can be destroyed (for example, if the player is far away). If not, we will check again after some time.



Let's try to destroy the object in the absence of the player
void ADestroyable::DestroyAfterBreaking()
{
    if (IsPlayerNear()) //  ,    
    {
        //  
        GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
    }
    else
    {
        GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //  
        Destroy(); //   
    }
}




Calling OnHit Node for Parts of an Object



In our case, Blueprints are responsible for the audiovisual part of the game, so we add Blueprints events where possible.



void ADestroyable::OnPartHitCallback(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
    OnPartHitBP(Hit, NormalImpulse, HitComp, OtherComp); // blueprint     
}


End Play and cleanup



Our game can be played in the default editor and some custom editors. That's why we need to clear everything we can in EndPlay.



void ADestroyable::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    /*   */
    GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle);
    GetWorld()->GetTimerManager().ClearTimer(ForceSleepTimerHandle);
    Super::EndPlay(EndPlayReason);
}


Configuration in Blueprints



The configuration is simple here. You simply place the pieces attached to the base mesh and mark them as "dest". That's all. Graphic artists don't need to do anything in the engine. Our base Blueprint class only does audiovisual stuff from events we provided in C ++. BeginPlay - downloads the required assets. In fact, in our case, each asset is a pointer to a program object, and you need to use them even when creating prototypes. Hard-coded asset references will increase editor / game load times and memory usage. On Break Event - responds to effects and appearance sounds. You can find some Niagara options here that will be described later. On Part Hit Event



image















image







image



- triggers impact effects and sounds.



image



A utility for quickly adding collisions



You can use Utility Blueprint to interact with assets to generate collisions for all parts of the object. It's much faster than creating them yourself.



image



image



Particle Effects in Niagara



The following describes how to create a simple effect in Niagara .







Material



image



image



The key to this material is the texture, not the shader, so it's really very simple.



Erosion, color and alpha are taken from Niagara.



image

Texture channel R Texture



image

channel G



Most of the effect is achieved by texture. Canal B could still be used to add more detail, but we don't need it at this time.



Niagara System Parameters



We use two Niagara systems: one for the burst effect (it uses a base mesh to spawn particles), and the other when parts collide (no static mesh position).



image

The user can specify the color and number of spawns and select a static mesh that will be used to select the location of the particle spawn



Niagara spawn burst



image

Here the user int32 is involved in order to be able to adjust the appearance counter for each destructible object



Niagara Particle Spawn



image



  • Selecting a static mesh from destructible objects;
  • Set random Lifetime, weight and size;
  • Choose a color from the custom ones (it is set by the destructible actor);
  • Create particles at the mesh vertices,
  • Add random speed and rotation speed.


Using a static grid



To be able to use static mesh in Niagara, your mesh must have the AllowCPU checkbox checked.



image



TIP: In the current (4.24) version of the engine, if you re-import your mesh, this value will be reset to the default. And in a shipping build, if you try to run a Niagara system with a mesh that doesn't have CPU access enabled, it will crash.



So let's add some simple code to check if the grid has this value.



bool UFunctionLibrary::MeshHaveCPUAccess(UStaticMesh* InMesh)
{
    return InMesh->bAllowCPUAccess;
}


It was used in Blueprints before Niagara.



image



You can create an editor widget to find destructible objects and set their Base Mesh variable to AllowCPUAccess.



Here's a Python code that looks for all destructible objects and sets CPU access to the underlying mesh.



Python code to set static grid allow_cpu_access variable
import unreal as ue

asset_registry = ue.AssetRegistryHelpers.get_asset_registry()
all_assets = asset_registry.get_assets_by_path('/Game/Blueprints/Actors/Destroyables', recursive=True) #   blueprints  
for asset in all_assets:
    path = asset.object_path
    bp_gc = ue.EditorAssetLibrary.load_blueprint_class(path) #get blueprint class
    bp_cdo = ue.get_default_object(bp_gc) # get the Class Default Object (CDO) of the generated class
    if bp_cdo.mesh.static_mesh != None:
        ue.EditorStaticMeshLibrary.set_allow_cpu_access(bp_cdo.mesh.static_mesh, True) # sets allow cpu on static mesh




You can run it directly with the py command, or create a button to run the code in the Utility Widget .



image



image



Niagara Particle Update



image



image



When updating, we do the following things:



  • Scaling Alpha Over Life,
  • Add curl noise,
  • Change the rotation speed in accordance with the expression: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ;
  • Scale the Size Over Life particle parameter,
  • Updating the material blur parameter,
  • Add a noise vector.


Why such a rather old-school approach?



Of course, you can use the current destruction system from UE4, but this way you can better control performance and visuals. When asked if you need a system as large as the built-in one for your needs, you must find the answer yourself. Because its use is often unreasonable.



As for Chaos, let's wait until it is ready for a full-fledged release, and then we'll look at its capabilities.



All Articles