Cómo crear objetos destructibles en Unreal Engine 4 y Blender





Los juegos modernos se están volviendo más realistas y una forma de lograrlo es crear entornos destructibles. Además, destrozar muebles, plantas, paredes, edificios y ciudades enteras es simplemente divertido.



Los ejemplos más llamativos de juegos con buena destructibilidad son Red Fraction: Guerrilla con su capacidad de perforar un túnel a través de Marte, Battlefield: Bad Company 2, donde puedes convertir todo el servidor en cenizas si quieres, y Control con su destrucción procedimental de todo lo que llama tu atención.



En 2019, Epic Games presentó una demostración del nuevo sistema de destrucción y física de alto rendimiento de Unreal , Chaos . El nuevo sistema permite crear destrucción de diferentes escalas, tiene soporte para el editor de efectos Niagara y al mismo tiempo se distingue por un uso económico de recursos.



Mientras tanto, Chaos está en prueba beta, hablemos de enfoques alternativos para crear objetos destructibles en Unreal Engine 4. En este artículo describiremos uno de ellos en detalle.





Requisitos



Comencemos enumerando lo que nos gustaría lograr:



  • Control artístico. Queremos que nuestros artistas puedan crear objetos destructibles como les plazca.
  • Destrucción que no afecta el juego. Deben ser puramente visuales, sin perturbar nada relacionado con el juego.
  • Mejoramiento. Queremos tener un control total sobre el rendimiento y no dejar que la CPU se apague.
  • Fácil de instalar. La configuración de tales objetos debe ser comprensible para los artistas, por lo tanto, es necesario que incluya solo los pasos mínimos necesarios.


Los entornos destructibles de Dark Souls 3 y Bloodborne se tomaron como referencia en este artículo.



imagen



idea principal



De hecho, la idea es simple:



  • Crea una malla de línea de base visible
  • Agrega partes ocultas de la malla;
  • Al destruir: ocultar la malla base -> mostrar sus partes -> iniciar la física.


imagen



imagen



Preparando activos



Usaremos Blender para preparar objetos. Para crear una malla a lo largo de la cual colapsarán, usamos un complemento de Blender llamado Cell Fracture.



Habilitando el complemento



Primero debemos habilitar el complemento, ya que está deshabilitado de forma predeterminada. Habilitación del complemento Cell Fracture



imagen





Complemento de búsqueda (F3)



Luego habilite el complemento en la cuadrícula seleccionada.



imagen



Ajustes de configuración



imagen



Lanzamiento del complemento



Mira el video, verifica la configuración desde allí. Asegúrese de configurar sus materiales correctamente.





Selección de material para desplegar piezas cortadas



Luego crearemos un mapa UV para estas partes.



imagen



imagen



Agregar división de borde



Edge Split arreglará el sombreado.



imagen



Modificadores de enlace



Usarlos aplicará Edge Split a todas las partes seleccionadas.



imagen



Terminación



Así es como se ve en Blender. Básicamente, no necesitamos modelar todas las piezas por separado.



imagen



Implementación



Clase base



Nuestro objeto destructible es un actor, que tiene varios componentes:



  • Escena de la raíz;
  • Malla estática - malla base;
  • Caja de colisión;
  • Caja de suelo;
  • Fuerza radial.


imagen



Cambiemos algunas configuraciones en el constructor:



  • Desactive la función de temporizador Tick (nunca olvide desactivarla para los actores que no la necesitan);
  • Configuramos la movilidad estática para todos los componentes;
  • Desactive la influencia en la navegación;
  • Configuración de perfiles de colisión.


Configurar un actor en el 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); 
}




En Begin Play, recopilamos algunos datos y los personalizamos:



  • Buscamos todas las partes con la etiqueta "dest";
  • Configure colisiones para todas las partes para que el artista no tenga que pensar en ello;
  • Establecer movilidad estática;
  • Ocultar todas las partes.


Configurar partes de un objeto en 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); //    ,        
    }
}




Función simple para obtener componentes
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;
}




Desencadenantes de destrucción



Hay tres formas de provocar la destrucción.



La



destrucción de OnOverlap ocurre cuando alguien lanza o usa un objeto que activa el proceso, como una bola rodante.



imagen



OnTakeDamage El



objeto que se destruye sufre daños.



imagen



OnOverlapWithNearDestroyable



En este caso, un objeto que es destructible se superpone a otro. En nuestro caso, por simplicidad, ambos se rompen.



imagen



Flujo de destrucción de objetos





imagen

Diagrama de destrucción de objetos



Muestra de partes 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 función principal de la destrucción.
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    
}




Qué hacer con la función dormir



Cuando se activa la función de suspensión, desactivamos la física / colisiones y configuramos la movilidad estática. Esto aumentará la productividad.



Cada componente primitivo de la física puede dormirse. Nos unimos a esta función en la destrucción.



Esta función puede ser inherente a cualquier primitiva. Nos unimos a él para completar la acción sobre el objeto.



A veces, el objeto físico no se duerme y continúa actualizándose, incluso si no ve ningún movimiento. Si continúa simulando la física, hacemos que todas sus partes se duerman después de 15 segundos.



Función de suspensión forzada llamada por temporizador
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
    InComp->SetSimulatePhysics(false); //   
    InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  
    InComp->SetMobility(EComponentMobility::Static); //      
    /*         */
}




Qué hacer con la destrucción



Necesitamos comprobar si el actor puede ser destruido (por ejemplo, si el jugador está lejos). Si no, lo comprobaremos nuevamente después de un tiempo.



Intentemos destruir el objeto en ausencia del jugador.
void ADestroyable::DestroyAfterBreaking()
{
    if (IsPlayerNear()) //  ,    
    {
        //  
        GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
    }
    else
    {
        GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //  
        Destroy(); //   
    }
}




Llamar al nodo OnHit para partes de un objeto



En nuestro caso, los Blueprints son responsables de la parte audiovisual del juego, por lo que agregamos eventos de Blueprints siempre que sea posible.



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


Fin de juego y limpieza



Nuestro juego se puede jugar en el editor predeterminado y algunos editores personalizados. Es por eso que necesitamos borrar todo lo que podamos en EndPlay.



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


Configuración en Blueprints



La configuración es simple aquí. Simplemente coloque las piezas unidas a la malla base y márquelas como "dest". Eso es todo. Los artistas gráficos no necesitan hacer nada en el motor. Nuestra clase básica Blueprint solo hace material audiovisual de eventos que proporcionamos en C ++. BeginPlay : descarga los recursos necesarios. De hecho, en nuestro caso, cada activo es un puntero a un objeto de programa y es necesario utilizarlos incluso al crear prototipos. Las referencias de activos codificadas aumentarán los tiempos de carga del editor / juego y el uso de memoria. On Break Event : responde a los efectos y sonidos de apariencia. Puede encontrar algunas opciones de Niagara aquí que se describirán más adelante. En evento de éxito parcial



imagen















imagen







imagen



- Activa efectos de impacto y sonidos.



imagen



Una utilidad para agregar colisiones rápidamente



Puede utilizar Utility Blueprint para interactuar con los activos y generar colisiones para todas las partes del objeto. Es mucho más rápido que crearlos usted mismo.



imagen



imagen



Efectos de las partículas en Niágara



A continuación se describe cómo crear un efecto simple en Niagara .







Material



imagen



imagen



La clave de este material es la textura, no el sombreador, por lo que es realmente muy simple.



La erosión, el color y el alfa se toman de Niagara.



imagen

Canal de textura R Canal de textura



imagen

G



La mayor parte del efecto se logra mediante la textura. El Canal B aún podría usarse para agregar más detalles, pero no lo necesitamos en este momento.



Parámetros del sistema Niagara



Usamos dos sistemas Niagara: uno para el efecto de explosión (usa una malla base para generar partículas) y el otro cuando las partes chocan (sin posición de malla estática).



imagen

El usuario puede especificar el color y el número de engendros y seleccionar una malla estática que se utilizará para seleccionar la ubicación del engendro de partículas.



Explosión de generación de Niagara



imagen

Aquí el usuario int32 está involucrado para poder ajustar el contador de apariencia para cada objeto destructible



Engendro de partículas del Niágara



imagen



  • Seleccionar una malla estática de objetos destructibles;
  • Establezca la duración, el peso y el tamaño al azar;
  • Elija un color de los personalizados (lo establece el actor destructible);
  • Crea partículas en los vértices de la malla,
  • Agregue velocidad aleatoria y velocidad de rotación.


Usando una cuadrícula estática



Para poder usar una malla estática en Niagara, su malla debe tener marcada la casilla de verificación AllowCPU.



imagen



SUGERENCIA: En la versión actual (4.24) del motor, si vuelve a importar su malla, este valor se restablecerá al predeterminado. Y en una versión de envío, si intenta ejecutar un sistema Niagara con una malla que no tiene habilitado el acceso a la CPU, se bloqueará.



Así que agreguemos un código simple para verificar si la cuadrícula tiene este valor.



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


Se usó en Blueprints antes de Niagara.



imagen



Puede crear un widget de editor para encontrar objetos destructibles y establecer su variable Base Mesh en AllowCPUAccess.



Aquí hay un código de Python que busca todos los objetos destructibles y establece el acceso de la CPU a la malla subyacente.



Código Python para establecer la variable allow_cpu_access de cuadrícula estática
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




Puede ejecutarlo directamente con el comando py , o crear un botón para ejecutar el código en el widget de utilidad .



imagen



imagen



Actualización de partículas de Niagara



imagen



imagen



Al actualizar, hacemos lo siguiente:



  • Escalando Alpha Over Life,
  • Agregue ruido de rizo,
  • Cambie la velocidad de rotación de acuerdo con la expresión: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ;
  • Escale el parámetro de partícula Size Over Life,
  • Actualizando el parámetro de desenfoque de material,
  • Agrega un vector de ruido.


¿Por qué un enfoque tan de la vieja escuela?



Por supuesto, puede usar el sistema de destrucción actual de UE4, pero de esta manera puede controlar mejor el rendimiento y las imágenes. Cuando se le pregunte si necesita un sistema tan grande como el integrado para sus necesidades, debe encontrar la respuesta usted mismo. Porque su uso a menudo no es razonable.



En cuanto a Chaos, esperemos hasta que esté listo para un lanzamiento completo, y luego veremos sus capacidades.



All Articles