Les jeux modernes deviennent de plus en plus réalistes, et une façon d'y parvenir est de créer des environnements destructibles. De plus, casser des meubles, des plantes, des murs, des bâtiments et des villes entières est tout simplement amusant.
Les exemples les plus frappants de jeux avec une bonne destructibilité sont Red Fraction: Guerrilla avec sa capacité à traverser Mars, Battlefield: Bad Company 2, où vous pouvez transformer tout le serveur en cendres si vous le souhaitez, et Control avec sa destruction procédurale de tout ce qui attire votre attention.
En 2019, Epic Games a dévoilé une démo du nouveau système de physique et de destruction hautes performances d' Unreal , Chaos . Le nouveau système vous permet de créer des destructions de différentes échelles, prend en charge l'éditeur d'effets Niagara et en même temps est économique en ressources.
En attendant, Chaos est en test bêta, parlons d'approches alternatives pour créer des objets destructibles dans Unreal Engine 4. Dans cet article, nous allons décrire l'une d'entre elles en détail.
Exigences
Commençons par énumérer ce que nous souhaitons réaliser:
- Contrôle artistique. Nous voulons que nos artistes soient capables de créer des objets destructibles à leur guise.
- Destruction qui n'affecte pas le gameplay. Ils doivent être purement visuels, ne déranger rien en rapport avec le gameplay.
- Optimisation. Nous voulons avoir un contrôle total sur les performances et ne pas laisser le processeur tomber en panne.
- Facile à installer. La configuration de la configuration de tels objets doit être compréhensible pour les artistes, il est donc nécessaire qu'elle ne comprenne que le minimum d'étapes nécessaire.
Les environnements destructibles de Dark Souls 3 et Bloodborne ont été pris comme référence dans cet article.
idée principale
En fait, l'idée est simple:
- Créez un maillage de ligne de base visible;
- Ajoutez des parties cachées du maillage;
- En cas de destruction: masquez le maillage de base -> affichez ses parties -> lancez la physique.
Préparation des actifs
Nous utiliserons Blender pour préparer des objets. Pour créer un maillage le long duquel ils se réduiront, nous utilisons un module complémentaire de Blender appelé Cell Fracture.
Activer l'addon
Nous devons d'abord activer l'addon car il est désactivé par défaut. Activation du module complémentaire Cell Fracture
Recherche addon (F3)
Ensuite, activez l'addon sur la grille sélectionnée.
Paramètres de configuration
Lancement de l'addon
Regardez la vidéo, vérifiez les paramètres à partir de là. Assurez-vous de configurer correctement vos matériaux.
Sélection des matériaux pour déplier les pièces coupées
Ensuite, nous allons créer une carte UV pour ces pièces.
Ajout d'une division d'arête
Edge Split corrigera l'ombrage.
Modificateurs de lien
Leur utilisation appliquera Edge Split à toutes les pièces sélectionnées.
Achèvement
Voici à quoi cela ressemble dans Blender. En gros, nous n'avons pas besoin de modéliser toutes les pièces séparément.
la mise en oeuvre
Classe de base
Notre objet destructible est un acteur, qui a plusieurs composants:
- Scène racine;
- Maillage statique - maillage de base;
- Boîte de collision;
- Boîte au sol;
- Force radiale.
Modifions certains paramètres dans le constructeur:
- Désactivez la fonction de minuterie Tick (n'oubliez jamais de la désactiver pour les acteurs qui n'en ont pas besoin);
- Nous mettons en place une mobilité statique pour tous les composants;
- Désactivez l'influence sur la navigation;
- Configuration des profils de collision.
Mettre en place un acteur dans le constructeur
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);
}
Dans Begin Play, nous collectons des données et les personnalisons:
- Nous recherchons toutes les pièces avec la balise "dest";
- Mettre en place des collisions pour toutes les pièces afin que l'artiste n'ait pas à y penser;
- Établir une mobilité statique;
- Cachez toutes les pièces.
Configuration de parties d'un objet dans 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); // ,
}
}
Fonction simple pour obtenir des composants
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;
}
Déclencheurs de destruction
Il existe trois façons de provoquer la destruction.
OnOverlap
Destruction se produit lorsque quelqu'un lance ou utilise autrement un objet qui active le processus, tel qu'une balle qui roule.
OnTakeDamage L'
objet détruit subit des dégâts.
OnOverlapWithNearDestroyable
Dans ce cas, un objet destructible en chevauche un autre. Dans notre cas, pour simplifier, ils se cassent tous les deux.
Flux de destruction d'objets
Diagramme de destruction d'objets
Afficher les pièces destructibles
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); //
}
}
La fonction principale de la 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
}
Que faire avec la fonction de sommeil
Lorsque la fonction Sleep est déclenchée, nous désactivons la physique / les collisions et définissons la mobilité statique. Cela augmentera la productivité.
Chaque composant primitif de la physique peut s'endormir. Nous nous attachons à cette fonction lors de la destruction.
Cette fonction peut être inhérente à n'importe quelle primitive. Nous nous y lions pour terminer l'action sur l'objet.
Parfois, l'objet physique ne s'endort pas et continue de se mettre à jour, même si vous ne voyez aucun mouvement. S'il continue à simuler la physique, nous mettons toutes ses parties en veille après 15 secondes.
Fonction de veille forcée appelée par la minuterie
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
InComp->SetSimulatePhysics(false); //
InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //
InComp->SetMobility(EComponentMobility::Static); //
/* */
}
Que faire de la destruction
Nous devons vérifier si l'acteur peut être détruit (par exemple, si le joueur est loin). Sinon, nous vérifierons à nouveau après un certain temps.
Essayons de détruire l'objet en l'absence du joueur
void ADestroyable::DestroyAfterBreaking()
{
if (IsPlayerNear()) // ,
{
//
GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
}
else
{
GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //
Destroy(); //
}
}
Appel d'un nœud OnHit pour des parties d'un objet
Dans notre cas, les Blueprints sont responsables de la partie audiovisuelle du jeu, nous ajoutons donc des événements Blueprints lorsque cela est possible.
void ADestroyable::OnPartHitCallback(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
OnPartHitBP(Hit, NormalImpulse, HitComp, OtherComp); // blueprint
}
Terminer la lecture et le nettoyage
Notre jeu peut être joué dans l'éditeur par défaut et dans certains éditeurs personnalisés. C'est pourquoi nous devons effacer tout ce que nous pouvons dans EndPlay.
void ADestroyable::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
/* */
GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle);
GetWorld()->GetTimerManager().ClearTimer(ForceSleepTimerHandle);
Super::EndPlay(EndPlayReason);
}
Configuration dans les Blueprints
La configuration est simple ici. Vous placez simplement les pièces attachées au maillage de base et les marquez comme "dest". C'est tout. Les graphistes n'ont rien à faire dans le moteur. Notre classe Blueprint de base ne fait que des éléments audiovisuels à partir d'événements que nous avons fournis en C ++. BeginPlay - télécharge les ressources requises. En fait, dans notre cas, chaque actif est un pointeur vers un objet programme, et vous devez les utiliser même lors de la création de prototypes. Les références d'actifs codées en dur augmenteront les temps de chargement de l'éditeur / jeu et l'utilisation de la mémoire. On Break Event - répond aux effets et aux sons d'apparence. Vous pouvez trouver ici quelques options Niagara qui seront décrites plus tard. Sur l'événement de coup de pièce
- déclenche des effets et des sons d'impact.
Un utilitaire pour ajouter rapidement des collisions
Vous pouvez utiliser Utility Blueprint pour interagir avec les actifs afin de générer des collisions pour toutes les parties de l'objet. C'est beaucoup plus rapide que de les créer vous-même.
Effets de particules à Niagara
Ce qui suit décrit comment créer un effet simple dans Niagara .
Matériel
La clé de ce matériau est la texture, pas le shader, donc c'est vraiment très simple.
L'érosion, la couleur et l'alpha proviennent du Niagara.
Canal de texture R Canal de texture
G
La plupart de l'effet est obtenu par la texture. Canal B pourrait encore être utilisé pour ajouter plus de détails, mais nous n'en avons pas besoin pour le moment.
Paramètres du système Niagara
Nous utilisons deux systèmes Niagara: l'un pour l'effet d'éclatement (il utilise un maillage de base pour générer des particules), et l'autre lorsque des pièces entrent en collision (pas de position de maillage statique).
L'utilisateur peut spécifier la couleur et le nombre d'apparitions et sélectionner un maillage statique qui sera utilisé pour sélectionner l'emplacement du spawn des particules
Éclat de spawn du Niagara
Ici l'utilisateur int32 est impliqué afin de pouvoir ajuster le compteur d'apparence pour chaque objet destructible
Spawn de particules du Niagara
- Sélection d'un maillage statique à partir d'objets destructibles;
- Définir la durée de vie, le poids et la taille aléatoires;
- Choisissez une couleur parmi celles personnalisées (elle est définie par l'acteur destructible);
- Créer des particules aux sommets du maillage,
- Ajoutez une vitesse aléatoire et une vitesse de rotation.
Utilisation d'une grille statique
Pour pouvoir utiliser le maillage statique dans Niagara, votre maillage doit avoir la case à cocher AllowCPU cochée.
CONSEIL: Dans la version actuelle (4.24) du moteur, si vous réimportez votre maillage, cette valeur sera réinitialisée à la valeur par défaut. Et dans une version d'expédition, si vous essayez d'exécuter un tel système Niagara avec un maillage qui n'a pas accès au processeur activé, il plantera.
Ajoutons donc un code simple pour vérifier si la grille est définie sur cette valeur.
bool UFunctionLibrary::MeshHaveCPUAccess(UStaticMesh* InMesh)
{
return InMesh->bAllowCPUAccess;
}
Il a été utilisé dans Blueprints avant Niagara.
Vous pouvez créer un widget éditeur pour trouver des objets destructibles et définir leur variable Base Mesh sur AllowCPUAccess.
Voici un code Python qui recherche tous les objets destructibles et définit l'accès du processeur au maillage sous-jacent.
Code Python pour définir la variable de grille statique 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
Vous pouvez l'exécuter directement avec la commande py ou créer un bouton pour exécuter le code dans le widget utilitaire .
Mise à jour sur les particules du Niagara
Lors de la mise à jour, nous faisons les choses suivantes:
- Mise à l'échelle de l'Alpha au fil de la vie,
- Ajoutez du bruit de boucle,
- Modifiez la vitesse de rotation conformément à l'expression: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ;
- Mettre à l'échelle le paramètre de particule Size Over Life,
- Mise à jour du paramètre de flou de matière,
- Ajoutez un vecteur de bruit.
Pourquoi une telle approche plutôt old-school?
Bien sûr, vous pouvez utiliser le système de destruction actuel de UE4, mais de cette façon, vous pouvez mieux contrôler les performances et les visuels. Lorsqu'on vous demande si vous avez besoin d'un système aussi grand que celui intégré pour vos besoins, vous devez trouver la réponse vous-même. Parce que son utilisation est souvent déraisonnable.
Quant à Chaos, attendons qu'il soit prêt pour une version à part entière, puis nous examinerons ses capacités.