Comment créer des objets destructibles dans Unreal Engine 4 et Blender





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.



image



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.


image



image



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



image





Recherche addon (F3)



Ensuite, activez l'addon sur la grille sélectionnée.



image



Paramètres de configuration



image



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.



image



image



Ajout d'une division d'arête



Edge Split corrigera l'ombrage.



image



Modificateurs de lien



Leur utilisation appliquera Edge Split à toutes les pièces sélectionnées.



image



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.



image



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.


image



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.



image



OnTakeDamage L'



objet détruit subit des dégâts.



image



OnOverlapWithNearDestroyable



Dans ce cas, un objet destructible en chevauche un autre. Dans notre cas, pour simplifier, ils se cassent tous les deux.



image



Flux de destruction d'objets





image

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



image















image







image



- déclenche des effets et des sons d'impact.



image



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.



image



image



Effets de particules à Niagara



Ce qui suit décrit comment créer un effet simple dans Niagara .







Matériel



image



image



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.



image

Canal de texture R Canal de texture



image

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).



image

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



image

Ici l'utilisateur int32 est impliqué afin de pouvoir ajuster le compteur d'apparence pour chaque objet destructible



Spawn de particules du Niagara



image



  • 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.



image



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.



image



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 .



image



image



Mise à jour sur les particules du Niagara



image



image



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.



All Articles