Cara membuat objek yang dapat dirusak di Unreal Engine 4 dan Blender





Game modern menjadi lebih realistis, dan salah satu cara untuk mencapainya adalah dengan menciptakan lingkungan yang dapat dirusak. Plus, menghancurkan furnitur, tanaman, tembok, gedung, dan seluruh kota itu menyenangkan.



Contoh paling mencolok dari game dengan kemampuan destruktif yang baik adalah Red Fraction: Guerrilla, dengan kemampuannya untuk menerobos Mars, Battlefield: Bad Company 2, di mana Anda dapat mengubah seluruh server menjadi abu jika Anda mau, dan Kontrol dengan penghancuran prosedural dari semua yang menarik perhatian Anda.



Pada tahun 2019, Epic Games meluncurkan demo sistem fisika dan penghancuran performa tinggi Unreal yang baru, Chaos . Sistem baru memungkinkan Anda untuk membuat penghancuran skala yang berbeda, memiliki dukungan untuk editor efek Niagara, dan pada saat yang sama menghemat sumber daya.



Sementara itu, Chaos sedang dalam pengujian beta, mari kita bicara tentang pendekatan alternatif untuk membuat objek yang dapat dirusak di Unreal Engine 4. Dalam artikel ini kami akan menjelaskan salah satunya secara mendetail.





Persyaratan



Mari kita mulai dengan membuat daftar apa yang ingin kita capai:



  • Kontrol artistik. Kami ingin seniman kami dapat membuat objek yang dapat dirusak sesuka mereka.
  • Kehancuran yang tidak memengaruhi gameplay. Mereka harus murni visual, tidak mengganggu apapun yang berhubungan dengan gameplay.
  • Optimasi. Kami ingin memiliki kendali penuh atas kinerja dan tidak membiarkan CPU turun.
  • Mudah dipasang. Menyiapkan konfigurasi objek semacam itu harus dapat dimengerti oleh seniman, oleh karena itu perlu hanya memasukkan langkah-langkah minimum yang diperlukan.


Lingkungan yang dapat dihancurkan dari Dark Souls 3 dan Bloodborne diambil sebagai referensi dalam artikel ini.



gambar



ide utama



Sebenarnya, idenya sederhana:



  • Buat jaring dasar yang terlihat;
  • Tambahkan bagian mesh yang tersembunyi;
  • Saat dihancurkan: sembunyikan mesh dasar -> tunjukkan bagian-bagiannya -> mulai fisika.


gambar



gambar



Mempersiapkan aset



Kami akan menggunakan Blender untuk menyiapkan objek. Untuk membuat mesh di mana mereka akan runtuh, kami menggunakan add-on Blender yang disebut Fraktur Sel.



Mengaktifkan addon



Pertama kita perlu mengaktifkan addon karena dinonaktifkan secara default. Mengaktifkan addon Fraktur Sel



gambar





Pencarian addon (F3)



Kemudian aktifkan addon di kisi yang dipilih.



gambar



Pengaturan konfigurasi



gambar



Meluncurkan addon



Tonton videonya, periksa pengaturan dari sana. Pastikan Anda menyiapkan materi dengan benar.





Pemilihan bahan untuk membuka potongan potongan



Kemudian kita akan membuat peta UV untuk bagian ini.



gambar



gambar



Menambahkan Edge Split



Edge Split akan memperbaiki bayangan.



gambar



Pengubah tautan



Menggunakannya akan menerapkan Edge Split ke semua bagian yang dipilih.



gambar



Penyelesaian



Ini adalah tampilannya di Blender. Pada dasarnya, kita tidak perlu memodelkan semua bagian secara terpisah.



gambar



Penerapan



Kelas dasar



Objek kami yang dapat dirusak adalah Aktor, yang memiliki beberapa komponen:



  • Adegan root;
  • Mesh Statis - mesh dasar;
  • Kotak tabrakan;
  • Kotak lantai;
  • Gaya radial.


gambar



Mari ubah beberapa pengaturan di konstruktor:



  • Nonaktifkan fitur Tick timer (jangan pernah lupa untuk menonaktifkannya untuk aktor yang tidak membutuhkannya);
  • Kami menyiapkan mobilitas statis untuk semua komponen;
  • Nonaktifkan pengaruh pada navigasi;
  • Mengonfigurasi profil tabrakan.


Menyiapkan aktor di 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); 
}




Di Begin Play, kami mengumpulkan beberapa data dan menyesuaikannya:



  • Kami mencari semua bagian dengan tag "dest";
  • Atur tabrakan untuk semua bagian sehingga artis tidak perlu memikirkannya;
  • Tetapkan mobilitas statis;
  • Sembunyikan semua bagian.


Menyiapkan bagian dari suatu objek di 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); //    ,        
    }
}




Fungsi sederhana untuk mendapatkan bagian komponen
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;
}




Pemicu kehancuran



Ada tiga cara untuk memprovokasi kehancuran. Penghancuran



OnOverlap



terjadi ketika seseorang melempar atau menggunakan objek yang mengaktifkan suatu proses, seperti bola bergulir.



gambar



OnTakeDamage



Objek yang dihancurkan menerima kerusakan.



gambar



OnOverlapWithNearDestroyable



Dalam kasus ini, satu objek yang dapat dirusak tumpang tindih dengan yang lain. Dalam kasus kami, untuk kesederhanaan, keduanya rusak.



gambar



Aliran penghancuran objek





gambar

Diagram penghancuran objek



Menunjukkan bagian yang dapat dirusak
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); //      
    }
}




Fungsi utama penghancuran
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    
}




Apa yang harus dilakukan dengan fungsi tidur



Saat fungsi Tidur dipicu, kami menonaktifkan fisika / tabrakan dan mengatur mobilitas statis. Ini akan meningkatkan produktivitas.



Setiap komponen primitif dengan fisika bisa tidur. Kami mengikat fungsi ini pada penghancuran.



Fungsi ini bisa melekat pada primitif manapun. Kami mengikatnya untuk menyelesaikan tindakan pada objek.



Terkadang objek fisik tidak tertidur dan terus diperbarui, meskipun Anda tidak melihat gerakan apa pun. Jika terus mensimulasikan fisika, kami membuat semua bagiannya tidur setelah 15 detik.



Fungsi tidur paksa disebut dengan timer
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
    InComp->SetSimulatePhysics(false); //   
    InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //  
    InComp->SetMobility(EComponentMobility::Static); //      
    /*         */
}




Apa yang harus dilakukan dengan kehancuran



Kita perlu memeriksa apakah aktornya bisa dihancurkan (misalnya, jika pemainnya jauh). Jika tidak, kami akan memeriksanya lagi setelah beberapa waktu.



Mari kita coba untuk menghancurkan objek tanpa kehadiran pemain
void ADestroyable::DestroyAfterBreaking()
{
    if (IsPlayerNear()) //  ,    
    {
        //  
        GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
    }
    else
    {
        GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //  
        Destroy(); //   
    }
}




Memanggil OnHit Node untuk Bagian dari sebuah Objek



Dalam kasus kami, Cetak Biru bertanggung jawab atas bagian audiovisual game, jadi kami menambahkan acara Cetak Biru jika memungkinkan.



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


Akhiri Play dan bersihkan



Game kami dapat dimainkan di editor default dan beberapa editor khusus. Itulah mengapa kami perlu menghapus semua yang kami bisa di EndPlay.



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


Konfigurasi dalam Cetak Biru



Konfigurasinya sederhana di sini. Anda cukup menempatkan potongan-potongan yang menempel pada jaring dasar dan menandainya sebagai "tujuan". Itu saja. Seniman grafis tidak perlu melakukan apa pun di mesin. Kelas Blueprint dasar kami hanya melakukan hal-hal audiovisual dari acara yang kami sediakan dalam C ++. BeginPlay - mengunduh aset yang diperlukan. Faktanya, dalam kasus kami, setiap aset adalah penunjuk ke objek program, dan Anda perlu menggunakannya bahkan saat membuat prototipe. Referensi aset dengan kode keras akan meningkatkan waktu muat editor / game dan penggunaan memori. On Break Event - merespons efek dan suara penampilan. Anda dapat menemukan beberapa opsi Niagara di sini yang akan dijelaskan nanti. Pada Part Hit Event



gambar















gambar







gambar



- memicu efek benturan dan suara.



gambar



Sebuah utilitas untuk menambahkan tabrakan dengan cepat



Anda dapat menggunakan Cetak Biru Utilitas untuk berinteraksi dengan aset guna menghasilkan benturan untuk semua bagian objek. Ini jauh lebih cepat daripada membuatnya sendiri.



gambar



gambar



Efek Partikel di Niagara



Berikut ini menjelaskan cara membuat efek sederhana di Niagara .







Bahan



gambar



gambar



Kunci dari bahan ini adalah teksturnya, bukan shadernya, jadi ini sangat sederhana.



Erosi, warna dan alpha diambil dari Niagara.



gambar

Saluran tekstur R Saluran tekstur



gambar

G



Sebagian besar efek dicapai oleh tekstur. Kanal B masih bisa digunakan untuk menambahkan lebih banyak detail, tetapi kami tidak membutuhkannya saat ini.



Parameter Sistem Niagara



Kami menggunakan dua sistem Niagara, satu untuk efek ledakan (menggunakan jaring dasar untuk menelurkan partikel), dan yang lainnya ketika bagian bertabrakan (tidak ada posisi jaring statis).



gambar

Pengguna dapat menentukan warna dan jumlah pemijahan dan memilih jaring statis yang akan digunakan untuk memilih lokasi pemijahan partikel.



Niagara bertelur meledak



gambar

Di sini pengguna int32 dilibatkan agar dapat menyesuaikan penghitung tampilan untuk setiap objek yang dapat dirusak



Niagara Particle Spawn



gambar



  • Memilih mesh statis dari objek yang dapat dirusak;
  • Atur Lifetime, berat dan ukuran acak;
  • Pilih warna dari yang khusus (ini diatur oleh aktor yang dapat dirusak);
  • Buat partikel di simpul mesh,
  • Tambahkan kecepatan acak dan kecepatan rotasi.


Menggunakan grid statis



Untuk dapat menggunakan mesh statis di Niagara, mesh Anda harus mencentang kotak AllowCPU.



gambar



TIPS: Dalam versi mesin saat ini (4.24), jika Anda mengimpor kembali mesh Anda, nilai ini akan disetel ulang ke default. Dan dalam pembuatan pengiriman, jika Anda mencoba menjalankan sistem Niagara dengan mesh yang tidak mengaktifkan akses CPU, itu akan macet.



Jadi mari tambahkan beberapa kode sederhana untuk memeriksa apakah grid disetel ke nilai ini.



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


Itu digunakan dalam Blueprints sebelum Niagara.



gambar



Anda dapat membuat widget editor untuk menemukan objek yang dapat dirusak dan menyetel variabel Base Mesh ke AllowCPUAccess.



Berikut kode Python yang mencari semua objek yang dapat dirusak dan menyetel akses CPU ke mesh yang mendasarinya.



Kode Python untuk mengatur variabel allow_cpu_access grid statis
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




Anda dapat menjalankannya langsung dengan perintah py , atau membuat tombol untuk menjalankan kode di Widget Utilitas .



gambar



gambar



Pembaruan Partikel Niagara



gambar



gambar



Saat memperbarui, kami melakukan hal-hal berikut:



  • Scaling Alpha Over Life,
  • Tambahkan kebisingan keriting,
  • Ubah kecepatan rotasi sesuai dengan ekspresi: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ;
  • Skala parameter partikel Size Over Life,
  • Memperbarui parameter material blur,
  • Tambahkan vektor kebisingan.


Mengapa pendekatan yang agak kuno?



Tentu saja, Anda dapat menggunakan sistem penghancuran saat ini dari UE4, tetapi dengan cara ini Anda dapat mengontrol kinerja dan visual dengan lebih baik. Ketika ditanya apakah Anda membutuhkan sistem sebesar yang terintegrasi untuk kebutuhan Anda, Anda harus menemukan jawabannya sendiri. Karena penggunaannya seringkali tidak masuk akal.



Sedangkan untuk Chaos, mari kita tunggu sampai siap untuk rilis lengkap, lalu kita akan melihat kemampuannya.



All Articles