Como criar objetos destrutíveis no Unreal Engine 4 e no Blender





Os jogos modernos estão se tornando mais realistas e uma maneira de conseguir isso é criar ambientes destrutíveis. Além disso, destruir móveis, plantas, paredes, edifícios e cidades inteiras é muito divertido.



Os exemplos mais notáveis ​​de jogos com boa destrutibilidade são Red Fraction: Guerrilla, com sua capacidade de criar um túnel através de Marte, Battlefield: Bad Company 2, onde você pode transformar todo o servidor em cinzas se quiser, e Control com sua destruição processual de tudo que chamar sua atenção.



Em 2019, a Epic Games revelou uma demonstração do novo sistema de física e destruição de alto desempenho do Unreal , Chaos . O novo sistema permite criar destruição de diferentes escalas, tem suporte para o editor de efeitos Niágara, e ao mesmo tempo é econômico no gasto de recursos.



Enquanto isso, Chaos está em teste beta, vamos falar sobre abordagens alternativas para criar objetos destrutíveis no Unreal Engine 4. Neste artigo, descreveremos um deles em detalhes.





Requisitos



Vamos começar listando o que gostaríamos de alcançar:



  • Controle artístico. Queremos que nossos artistas sejam capazes de criar objetos destrutíveis como quiserem.
  • Destruição que não afeta o jogo. Eles devem ser puramente visuais, não perturbando nada relacionado à jogabilidade.
  • Otimização. Queremos ter controle total sobre o desempenho e não deixar o CPU cair.
  • Fácil de instalar. A configuração de tais objetos deve ser compreensível para os artistas, portanto é necessário que inclua apenas o mínimo de etapas necessárias.


Ambientes destrutíveis de Dark Souls 3 e Bloodborne foram tomados como referência neste artigo.



imagem



Ideia principal



Na verdade, a ideia é simples:



  • Crie uma malha de linha de base visível;
  • Adicione partes ocultas da malha;
  • Na destruição: esconda a malha base -> mostre suas partes -> inicie a física.


imagem



imagem



Preparando ativos



Usaremos o Blender para preparar objetos. Para criar uma malha ao longo da qual eles entrarão em colapso, usamos um complemento do Blender chamado Cell Fracture.



Habilitando o addon



Primeiro, precisamos habilitar o addon, pois ele está desabilitado por padrão. Complemento Habilitando Fratura Celular



imagem





Addon de pesquisa (F3)



Em seguida, ative o addon na grade selecionada.



imagem



Definições de configuração



imagem



Lançamento do complemento



Assista ao vídeo, verifique as configurações a partir daí. Certifique-se de configurar seus materiais corretamente.





Seleção de material para desdobramento de peças cortadas



Em seguida, criaremos um mapa UV para essas partes.



imagem



imagem



Adicionando Divisão de Borda



O Edge Split corrigirá o sombreamento.



imagem



Modificadores de link



Usá-los aplicará a divisão de aresta a todas as peças selecionadas.



imagem



Conclusão



É assim que fica no Blender. Basicamente, não precisamos modelar todas as peças separadamente.



imagem



Implementação



Classe base



Nosso objeto destrutível é um Ator, que possui vários componentes:



  • Cena raiz;
  • Malha estática - malha de base;
  • Caixa de colisão;
  • Caixa de chão;
  • Força radial.


imagem



Vamos alterar algumas configurações no construtor:



  • Desabilite o recurso Tick timer (nunca se esqueça de desabilitá-lo para atores que não precisam dele);
  • Configuramos mobilidade estática para todos os componentes;
  • Desative a influência na navegação;
  • Configurando perfis de colisão.


Configurando um ator no construtor
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); 
}




No Begin Play, coletamos alguns dados e os personalizamos:



  • Procuramos todas as peças com a etiqueta "dest";
  • Configure colisões para todas as partes para que o artista não tenha que pensar sobre isso;
  • Estabeleça mobilidade estática;
  • Oculte todas as peças.


Configurando partes de um objeto em 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); //    ,        
    }
}




Função simples para obter peças 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;
}




Gatilhos de destruição



Existem três maneiras de provocar destruição.



A



destruição OnOverlap ocorre quando alguém arremessa ou usa um objeto que ativa o processo, como uma bola rolando.



imagem



OnTakeDamage O



objeto sendo destruído sofre dano.



imagem



OnOverlapWithNearDestroyable



Nesse caso, um objeto sendo destrutível se sobrepõe a outro. No nosso caso, para simplificar, ambos quebram.



imagem



Fluxo de destruição de objetos





imagem

Diagrama de destruição de objetos



Mostrar peças destrutíveis
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); //      
    }
}




A principal função de destruição
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    
}




O que fazer com a função sono



Quando a função Sleep é acionada, desabilitamos a física / colisões e configuramos a mobilidade estática. Isso aumentará a produtividade.



Cada componente primitivo da física pode adormecer. Nós nos ligamos a essa função na destruição.



Esta função pode ser inerente a qualquer primitiva. Ligamos a ele para completar a ação no objeto.



Às vezes, o objeto físico não adormece e continua a se atualizar, mesmo que você não veja nenhum movimento. Se continuar a simular a física, fazemos todas as suas partes dormirem após 15 segundos.



Função de sono forçado chamada por temporizador
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
    InComp->SetSimulatePhysics(false); //   
    InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  
    InComp->SetMobility(EComponentMobility::Static); //      
    /*         */
}




O que fazer com a destruição



Precisamos verificar se o ator pode ser destruído (por exemplo, se o jogador estiver longe). Caso contrário, verificaremos novamente após algum tempo.



Vamos tentar destruir o objeto na ausência do jogador
void ADestroyable::DestroyAfterBreaking()
{
    if (IsPlayerNear()) //  ,    
    {
        //  
        GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
    }
    else
    {
        GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //  
        Destroy(); //   
    }
}




Chamando OnHit Node para partes de um objeto



No nosso caso, os Blueprints são responsáveis ​​pela parte audiovisual do jogo, então adicionamos eventos Blueprints sempre que possível.



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


Fim do jogo e limpeza



Nosso jogo pode ser jogado no editor padrão e em alguns editores personalizados. É por isso que precisamos limpar tudo o que pudermos no EndPlay.



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


Configuração em Blueprints



A configuração é simples aqui. Você simplesmente coloca as peças presas à malha de base e as marca como "dest". Isso é tudo. Os artistas gráficos não precisam fazer nada no motor. Nossa classe base do Blueprint só faz material audiovisual de eventos que fornecemos em C ++. BeginPlay - baixa os ativos necessários. Na verdade, em nosso caso, cada ativo é um ponteiro para um objeto de programa e você precisa usá-los mesmo ao criar protótipos. As referências de ativos embutidos no código aumentarão os tempos de carregamento do editor / jogo e o uso de memória. On Break Event - responde a efeitos e sons de aparência. Você pode encontrar algumas opções de Niagara aqui que serão descritas mais tarde. On Part Hit Event



imagem















imagem







imagem



- aciona efeitos de impacto e sons.



imagem



Um utilitário para adicionar colisões rapidamente



Você pode usar o Utility Blueprint para interagir com ativos para gerar colisões para todas as partes do objeto. É muito mais rápido do que criá-los você mesmo.



imagem



imagem



Efeitos de partículas em Niágara



O seguinte descreve como criar um efeito simples no Niágara .







Material



imagem



imagem



A chave para este material é a textura, não o shader, então é realmente muito simples.



Erosão, cor e alfa são retirados do Niágara.



imagem

Canal de textura R Canal de textura



imagem

G



A maior parte do efeito é obtida pela textura. O Canal B ainda pode ser usado para adicionar mais detalhes, mas não precisamos dele no momento.



Parâmetros do sistema Niágara



Usamos dois sistemas Niagara: um para o efeito de explosão (usa uma malha de base para gerar partículas) e o outro quando as partes colidem (sem posição de malha estática).



imagem

O usuário pode especificar a cor e o número de spawns e selecionar uma malha estática que será usada para selecionar a localização do spawn de partícula



Niagara spawn burst



imagem

Aqui, o usuário int32 está envolvido para poder ajustar o contador de aparência para cada objeto destrutível



Niagara Particle Spawn



imagem



  • Selecionando uma malha estática de objetos destrutíveis;
  • Defina o tempo de vida, peso e tamanho aleatórios;
  • Escolha uma cor entre as personalizadas (é definida pelo ator destrutível);
  • Crie partículas nos vértices da malha,
  • Adicione velocidade aleatória e velocidade de rotação.


Usando uma grade estática



Para poder usar a malha estática em Niagara, sua malha deve ter a caixa de seleção AllowCPU marcada.



imagem



DICA: Na versão atual (4.24) do motor, se você reimportar sua malha, este valor será redefinido para o padrão. E em uma compilação de remessa, se você tentar executar um sistema Niagara com uma malha que não tenha acesso à CPU habilitado, ele travará.



Portanto, vamos adicionar um código simples para verificar se a grade está configurada com esse valor.



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


Foi usado em projetos antes de Niagara.



imagem



Você pode criar um widget de editor para localizar objetos destrutíveis e definir sua variável Base Mesh para AllowCPUAccess.



Aqui está um código Python que procura todos os objetos destrutíveis e define o acesso da CPU para a malha subjacente.



Código Python para definir a variável de grade estática 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




Você pode executá-lo diretamente com o comando py ou criar um botão para executar o código no widget Utilitário .



imagem



imagem



Atualização Niagara Particle



imagem



imagem



Ao atualizar, fazemos o seguinte:



  • Dimensionando Alpha ao longo da vida,
  • Adicione ruído de ondulação,
  • Altere a velocidade de rotação de acordo com a expressão: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ;
  • Dimensione o parâmetro de partículas Size Over Life,
  • Atualizando o parâmetro material blur,
  • Adicione um vetor de ruído.


Por que uma abordagem tão tradicional?



Claro, você pode usar o sistema de destruição atual do UE4, mas desta forma você pode controlar melhor o desempenho e os visuais. Quando perguntado se você precisa de um sistema tão grande quanto o integrado para suas necessidades, você deve encontrar a resposta sozinho. Porque seu uso muitas vezes não é razoável.



Quanto ao Chaos, vamos esperar até que ele esteja pronto para um lançamento completo, e então veremos seus recursos.



All Articles