So erstellen Sie zerstörbare Objekte in Unreal Engine 4 und Blender





Moderne Spiele werden realistischer, und eine Möglichkeit, dies zu erreichen, besteht darin, zerstörbare Umgebungen zu schaffen. Außerdem macht es einfach Spaß, Möbel, Pflanzen, Wände, Gebäude und ganze Städte zu zerschlagen.



Die auffälligsten Beispiele für Spiele mit guter Zerstörbarkeit sind Red Fraction: Guerrilla mit seiner Fähigkeit, durch den Mars zu tunneln, Battlefield: Bad Company 2, wo Sie den gesamten Server in Asche verwandeln können, wenn Sie möchten, und Control mit seiner prozeduralen Zerstörung von allem, was Ihnen ins Auge fällt.



Im Jahr 2019 enthüllten Epic Games eine Demo von Unreals neuem Hochleistungs-Physik- und Zerstörungssystem Chaos . Das neue System ermöglicht die Zerstörung verschiedener Skalen, unterstützt den Niagara-Effekt-Editor und zeichnet sich gleichzeitig durch einen sparsamen Ressourceneinsatz aus.



In der Zwischenzeit befindet sich Chaos im Beta-Test. Lassen Sie uns über alternative Ansätze zum Erstellen zerstörbarer Objekte in Unreal Engine 4 sprechen. In diesem Artikel werden wir eines davon detailliert beschreiben.





Bedarf



Beginnen wir mit der Auflistung dessen, was wir erreichen möchten:



  • Künstlerische Kontrolle. Wir möchten, dass unsere Künstler zerstörbare Objekte nach Belieben herstellen können.
  • Zerstörung, die das Gameplay nicht beeinflusst. Sie sollten rein visuell sein und nichts im Zusammenhang mit dem Gameplay stören.
  • Optimierung. Wir möchten die vollständige Kontrolle über die Leistung haben und die CPU nicht ausfallen lassen.
  • Einfach zu installieren. Das Einrichten der Konfiguration solcher Objekte sollte für Künstler verständlich sein, daher ist es erforderlich, dass nur das erforderliche Minimum an Schritten enthalten ist.


Zerstörbare Umgebungen aus Dark Souls 3 und Bloodborne wurden in diesem Artikel als Referenz verwendet.



Bild



Hauptidee



In der Tat ist die Idee einfach:



  • Erstellen Sie ein sichtbares Grundliniennetz
  • Fügen Sie versteckte Teile des Netzes hinzu.
  • Bei Zerstörung: Das Grundnetz verstecken -> Teile anzeigen -> Physik starten.


Bild



Bild



Assets vorbereiten



Wir werden Blender verwenden, um Objekte vorzubereiten. Um ein Netz zu erstellen, entlang dessen sie zusammenfallen, verwenden wir ein Blender-Add-On namens Cell Fracture.



Addon aktivieren



Zuerst müssen wir das Addon aktivieren, da es standardmäßig deaktiviert ist. Aktivieren des Add-Ons für Zellfrakturen



Bild





Such-Addon (F3)



Aktivieren Sie dann das Addon im ausgewählten Raster.



Bild



Konfigurationseinstellungen



Bild



Addon starten



Sehen Sie sich das Video an und überprüfen Sie die Einstellungen von dort aus. Stellen Sie sicher, dass Sie Ihre Materialien richtig eingerichtet haben.





Materialauswahl zum Entfalten von geschnittenen Stücken



Dann erstellen wir eine UV-Karte für diese Teile.



Bild



Bild



Edge Split hinzufügen



Edge Split korrigiert die Schattierung.



Bild



Link-Modifikatoren



Wenn Sie sie verwenden, wird Edge Split auf alle ausgewählten Teile angewendet.



Bild



Fertigstellung



So sieht es in Blender aus. Grundsätzlich müssen wir nicht alle Teile separat modellieren.



Bild



Implementierung



Basisklasse



Unser zerstörbares Objekt ist ein Schauspieler, der mehrere Komponenten hat:



  • Wurzelszene;
  • Statisches Netz - Basisnetz;
  • Kollisionsbox;
  • Bodenbox;
  • Radialkraft.


Bild



Lassen Sie uns einige Einstellungen im Konstruktor ändern:



  • Deaktivieren Sie die Tick-Timer-Funktion (vergessen Sie niemals, sie für Schauspieler zu deaktivieren, die sie nicht benötigen).
  • Wir richten statische Mobilität für alle Komponenten ein.
  • Deaktivieren Sie den Einfluss auf die Navigation.
  • Kollisionsprofile konfigurieren.


Einrichten eines Akteurs im Konstruktor
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 sammeln wir einige Daten und passen sie an:



  • Wir suchen alle Teile mit dem Tag "dest".
  • Richten Sie Kollisionen für alle Teile ein, damit der Künstler nicht darüber nachdenken muss.
  • Statische Mobilität herstellen;
  • Verstecke alle Teile.


Einrichten von Teilen eines Objekts 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); //    ,        
    }
}




Einfache Funktion zum Abrufen von Bauteilen
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;
}




Zerstörung wird ausgelöst



Es gibt drei Möglichkeiten, Zerstörung zu provozieren.



OnOverlap



Destruction tritt auf, wenn jemand ein Objekt wirft oder anderweitig verwendet, das den Prozess aktiviert, z. B. einen rollenden Ball.



Bild



OnTakeDamage Das



zerstörte Objekt erleidet Schaden.



Bild



OnOverlapWithNearDestroyable



In diesem Fall überlappt ein zerstörbares Objekt ein anderes. In unserem Fall brechen beide der Einfachheit halber.



Bild



Objektzerstörungsfluss





Bild

Objektzerstörungsdiagramm



Ausstellung zerstörbarer Teile
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); //      
    }
}




Die Hauptfunktion der Zerstörung
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    
}




Was tun mit der Schlaffunktion?



Wenn die Schlaffunktion ausgelöst wird, deaktivieren wir Physik / Kollisionen und stellen die statische Mobilität ein. Dies erhöht die Produktivität.



Jede primitive Komponente mit Physik kann schlafen gehen. Wir binden an diese Funktion bei der Zerstörung.



Diese Funktion kann jedem Grundelement inhärent sein. Wir binden daran, um die Aktion für das Objekt abzuschließen.



Manchmal geht das physische Objekt nicht in den Ruhezustand und wird weiterhin aktualisiert, auch wenn Sie keine Bewegung sehen. Wenn es weiterhin die Physik simuliert, lassen wir alle Teile nach 15 Sekunden einschlafen.



Vom Timer aufgerufene erzwungene Schlaffunktion
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
    InComp->SetSimulatePhysics(false); //   
    InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  
    InComp->SetMobility(EComponentMobility::Static); //      
    /*         */
}




Was tun mit Zerstörung?



Wir müssen prüfen, ob der Schauspieler zerstört werden kann (zum Beispiel, wenn der Spieler weit weg ist). Wenn nicht, werden wir nach einiger Zeit erneut prüfen.



Versuchen wir, das Objekt in Abwesenheit des Spielers zu zerstören
void ADestroyable::DestroyAfterBreaking()
{
    if (IsPlayerNear()) //  ,    
    {
        //  
        GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
    }
    else
    {
        GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //  
        Destroy(); //   
    }
}




Aufrufen des OnHit-Knotens für Teile eines Objekts



In unserem Fall sind Blueprints für den audiovisuellen Teil des Spiels verantwortlich, daher fügen wir nach Möglichkeit Blueprints-Ereignisse hinzu.



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


Spiel beenden und aufräumen



Unser Spiel kann im Standardeditor und einigen benutzerdefinierten Editoren gespielt werden. Deshalb müssen wir in EndPlay alles löschen, was wir können.



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


Konfiguration in Blaupausen



Die Konfiguration ist hier einfach. Sie platzieren einfach die am Grundnetz befestigten Teile und markieren sie als "dest". Das ist alles. Grafiker müssen nichts in der Engine tun. Unsere Basis-Blueprint-Klasse führt nur audiovisuelle Inhalte von Ereignissen aus, die wir in C ++ bereitgestellt haben. BeginPlay - lädt die erforderlichen Assets herunter. In unserem Fall ist jedes Asset ein Zeiger auf ein Programmobjekt, und Sie müssen sie auch beim Erstellen von Prototypen verwenden. Fest codierte Asset-Referenzen erhöhen die Ladezeiten für Editoren / Spiele und die Speichernutzung. On Break Event - Reagiert auf Effekte und Erscheinungsgeräusche. Hier finden Sie einige Niagara-Optionen, die später beschrieben werden. On Part Hit Event



Bild















Bild







Bild



- löst Aufpralleffekte und Geräusche aus.



Bild



Ein Dienstprogramm zum schnellen Hinzufügen von Kollisionen



Mit Utility Blueprint können Sie mit Assets interagieren und Kollisionen für alle Teile des Objekts generieren. Es ist viel schneller als sie selbst zu erstellen.



Bild



Bild



Partikeleffekte in Niagara



Im Folgenden wird beschrieben, wie Sie in Niagara einen einfachen Effekt erstellen .







Material



Bild



Bild



Der Schlüssel zu diesem Material ist die Textur, nicht der Shader, also ist es wirklich sehr einfach.



Erosion, Farbe und Alpha stammen aus Niagara.



Bild

Texturkanal R Texturkanal



Bild

G Der



größte Teil des Effekts wird durch Textur erzielt. Kanal B könnte noch verwendet werden, um weitere Details hinzuzufügen, aber wir brauchen ihn derzeit nicht.



Niagara-Systemparameter



Wir verwenden zwei Niagara-Systeme: eines für den Burst-Effekt (es verwendet ein Basisnetz, um Partikel zu erzeugen) und das andere, wenn Teile kollidieren (keine statische Netzposition).



Bild

Der Benutzer kann die Farbe und Anzahl der Spawns angeben und ein statisches Netz auswählen, das zur Auswahl des Ortes des Partikel-Spawns verwendet wird



Niagara-Spawn platzte



Bild

Hier ist der Benutzer int32 beteiligt, um den Erscheinungszähler für jedes zerstörbare Objekt anpassen zu können



Niagara-Partikel-Spawn



Bild



  • Auswählen eines statischen Netzes aus zerstörbaren Objekten;
  • Stellen Sie zufällige Lebensdauer, Gewicht und Größe ein;
  • Wählen Sie eine Farbe aus den benutzerdefinierten aus (sie wird vom zerstörbaren Akteur festgelegt).
  • Erstellen Sie Partikel an den Netzscheitelpunkten.
  • Fügen Sie zufällige Geschwindigkeit und Rotationsgeschwindigkeit hinzu.


Verwenden eines statischen Gitters



Um statisches Netz in Niagara verwenden zu können, muss für Ihr Netz das Kontrollkästchen AllowCPU aktiviert sein.



Bild



TIPP: Wenn Sie in der aktuellen (4.24) Version der Engine Ihr Netz erneut importieren, wird dieser Wert auf den Standardwert zurückgesetzt. Wenn Sie in einem Versand-Build versuchen, ein solches Niagara-System mit einem Netz auszuführen, für das kein CPU-Zugriff aktiviert ist, stürzt es ab.



Fügen wir also einen einfachen Code hinzu, um zu überprüfen, ob das Raster auf diesen Wert eingestellt ist.



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


Es wurde in Blaupausen vor Niagara verwendet.



Bild



Sie können ein Editor-Widget erstellen, um zerstörbare Objekte zu finden, und ihre Basisnetzvariable auf AllowCPUAccess setzen.



Hier ist ein Python-Code, der nach allen zerstörbaren Objekten sucht und den CPU-Zugriff auf das zugrunde liegende Netz festlegt.



Python-Code zum Festlegen der statischen Raster-Variablen allow_cpu_access
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




Sie können es direkt mit dem Befehl py ausführen oder eine Schaltfläche erstellen, um den Code im Utility-Widget auszuführen .



Bild



Bild



Niagara-Partikel-Update



Bild



Bild



Beim Aktualisieren gehen wir wie folgt vor:



  • Alpha über das Leben skalieren,
  • Lockengeräusch hinzufügen,
  • Ändern Sie die Rotationsgeschwindigkeit gemäß dem Ausdruck: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ;
  • Skalieren Sie den Partikelparameter "Größe über Lebensdauer".
  • Aktualisieren des Materialunschärfeparameters,
  • Fügen Sie einen Rauschvektor hinzu.


Warum so ein eher altmodischer Ansatz?



Natürlich können Sie das aktuelle Zerstörungssystem von UE4 verwenden, aber auf diese Weise können Sie die Leistung und die Grafik besser steuern. Wenn Sie gefragt werden, ob Sie ein System benötigen, das für Ihre Anforderungen so groß ist wie das integrierte, müssen Sie die Antwort selbst finden. Weil seine Verwendung oft unvernünftig ist.



Was Chaos betrifft, warten wir, bis es für eine vollständige Veröffentlichung bereit ist, und dann werden wir uns seine Fähigkeiten ansehen.



All Articles