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.
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.
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
Pencarian addon (F3)
Kemudian aktifkan addon di kisi yang dipilih.
Pengaturan konfigurasi
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.
Menambahkan Edge Split
Edge Split akan memperbaiki bayangan.
Pengubah tautan
Menggunakannya akan menerapkan Edge Split ke semua bagian yang dipilih.
Penyelesaian
Ini adalah tampilannya di Blender. Pada dasarnya, kita tidak perlu memodelkan semua bagian secara terpisah.
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.
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.
OnTakeDamage
Objek yang dihancurkan menerima kerusakan.
OnOverlapWithNearDestroyable
Dalam kasus ini, satu objek yang dapat dirusak tumpang tindih dengan yang lain. Dalam kasus kami, untuk kesederhanaan, keduanya rusak.
Aliran penghancuran objek
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
- memicu efek benturan dan suara.
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.
Efek Partikel di Niagara
Berikut ini menjelaskan cara membuat efek sederhana di Niagara .
Bahan
Kunci dari bahan ini adalah teksturnya, bukan shadernya, jadi ini sangat sederhana.
Erosi, warna dan alpha diambil dari Niagara.
Saluran tekstur R Saluran tekstur
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).
Pengguna dapat menentukan warna dan jumlah pemijahan dan memilih jaring statis yang akan digunakan untuk memilih lokasi pemijahan partikel.
Niagara bertelur meledak
Di sini pengguna int32 dilibatkan agar dapat menyesuaikan penghitung tampilan untuk setiap objek yang dapat dirusak
Niagara Particle Spawn
- 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.
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.
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 .
Pembaruan Partikel Niagara
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.