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.
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.
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
Such-Addon (F3)
Aktivieren Sie dann das Addon im ausgewählten Raster.
Konfigurationseinstellungen
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.
Edge Split hinzufügen
Edge Split korrigiert die Schattierung.
Link-Modifikatoren
Wenn Sie sie verwenden, wird Edge Split auf alle ausgewählten Teile angewendet.
Fertigstellung
So sieht es in Blender aus. Grundsätzlich müssen wir nicht alle Teile separat modellieren.
Implementierung
Basisklasse
Unser zerstörbares Objekt ist ein Schauspieler, der mehrere Komponenten hat:
- Wurzelszene;
- Statisches Netz - Basisnetz;
- Kollisionsbox;
- Bodenbox;
- Radialkraft.
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.
OnTakeDamage Das
zerstörte Objekt erleidet Schaden.
OnOverlapWithNearDestroyable
In diesem Fall überlappt ein zerstörbares Objekt ein anderes. In unserem Fall brechen beide der Einfachheit halber.
Objektzerstörungsfluss
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
- löst Aufpralleffekte und Geräusche aus.
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.
Partikeleffekte in Niagara
Im Folgenden wird beschrieben, wie Sie in Niagara einen einfachen Effekt erstellen .
Material
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.
Texturkanal R Texturkanal
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).
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
Hier ist der Benutzer int32 beteiligt, um den Erscheinungszähler für jedes zerstörbare Objekt anpassen zu können
Niagara-Partikel-Spawn
- 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.
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.
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 .
Niagara-Partikel-Update
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.