أصبحت الألعاب الحديثة أكثر واقعية ، وإحدى طرق تحقيق ذلك هي إنشاء بيئات قابلة للتدمير. بالإضافة إلى ذلك ، فإن تحطيم الأثاث والنباتات والجدران والمباني والمدن بأكملها هو مجرد متعة.
أكثر الأمثلة المدهشة على الألعاب التي تتمتع بإمكانية تدمير جيدة هي Red Fraction: Guerrilla ، مع قدرتها على اختراق المريخ ، Battlefield: Bad Company 2 ، حيث يمكنك تحويل الخادم بأكمله إلى رماد إذا أردت ، والتحكم في تدميره الإجرائي لكل ما يلفت انتباهك.
في عام 2019 ، كشفت Epic Games النقاب عن عرض توضيحي لنظام التدمير والفيزياء عالي الأداء الجديد من Unreal ، Chaos . يسمح لك النظام الجديد بإنشاء تدمير لمقاييس مختلفة ، ولديه دعم لمحرر تأثير Niagara ، وفي نفس الوقت يتميز بالاستخدام الاقتصادي للموارد.
في هذه الأثناء ، Chaos في اختبار تجريبي ، فلنتحدث عن طرق بديلة لإنشاء كائنات قابلة للتدمير في Unreal Engine 4. في هذه المقالة ، سوف نصف أحدها بالتفصيل.
المتطلبات
لنبدأ بإدراج ما نرغب في تحقيقه:
- التحكم الفني. نريد لفنانينا أن يكونوا قادرين على صنع أشياء قابلة للتدمير كما يحلو لهم.
- الدمار الذي لا يؤثر على اللعب. يجب أن تكون بصرية بحتة ، ولا تزعج أي شيء متعلق بطريقة اللعب.
- الاقوي. نريد أن يكون لدينا سيطرة كاملة على الأداء وعدم ترك وحدة المعالجة المركزية تنخفض.
- سهل التنصيب. يجب أن يكون إعداد تكوين مثل هذه الأشياء مفهوماً للفنانين ، لذلك من الضروري أن يتضمن فقط الحد الأدنى الضروري من الخطوات.
البيئات المدمرة من Dark Souls 3 و Bloodborne تم أخذها كمرجع في هذه المقالة.
الفكرة الرئيسية
في الحقيقة ، الفكرة بسيطة:
- إنشاء شبكة أساسية مرئية ؛
- أضف الأجزاء المخفية من الشبكة ؛
- عند التدمير: إخفاء الشبكة الأساسية -> إظهار أجزائها -> بدء الفيزياء.
تجهيز الأصول
سوف نستخدم Blender لتحضير الأشياء. لإنشاء شبكة تنهار على طولها ، نستخدم إضافة Blender تسمى Cell Fracture.
تمكين الملحق
نحتاج أولاً إلى تمكين الملحق لأنه معطل افتراضيًا. تمكين الوظيفة الإضافية لكسر الخلية
ملحق البحث (F3)
ثم قم بتمكين الملحق على الشبكة المحددة.
إعدادات التكوين
إطلاق الملحق
شاهد الفيديو وتحقق من الإعدادات من هناك. تأكد من إعداد المواد الخاصة بك بشكل صحيح.
اختيار المواد لتكشف القطع المقطوعة
ثم سننشئ خريطة الأشعة فوق البنفسجية لهذه الأجزاء.
مضيفا حافة الانقسام
ستعمل Edge Split على إصلاح التظليل.
معدِّلات الارتباط
سيؤدي استخدامها إلى تطبيق Edge Split على جميع الأجزاء المحددة.
إكمال
هكذا تبدو في Blender. في الأساس ، لا نحتاج إلى نمذجة كل الأجزاء بشكل فردي.
التنفيذ
فئة أساسية
كائننا القابل للتدمير هو الفاعل ، والذي يتكون من عدة مكونات:
- مشهد الجذر
- شبكة ثابتة - شبكة قاعدة ؛
- صندوق الاصطدام
- صندوق أرضي
- القوة الشعاعية.
دعنا نغير بعض الإعدادات في المنشئ:
- تعطيل ميزة Tick timer (لا تنس أبدًا تعطيلها للممثلين الذين لا يحتاجونها) ؛
- قمنا بإعداد التنقل الثابت لجميع المكونات ؛
- تعطيل التأثير على الملاحة ؛
- تكوين ملفات تعريف التصادم.
إنشاء ممثل في المنشئ
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);
}
في Begin Play ، نجمع بعض البيانات ونخصصها:
- نحن نبحث عن جميع الأجزاء التي تحمل علامة "dest" ؛
- قم بإعداد تصادمات لجميع الأجزاء حتى لا يضطر الفنان إلى التفكير في الأمر ؛
- إنشاء التنقل الثابت ؛
- إخفاء جميع الأجزاء.
إعداد أجزاء من كائن في بدء اللعب
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); // ,
}
}
وظيفة بسيطة للحصول على الأجزاء المكونة
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;
}
مسببات التدمير
هناك ثلاث طرق لإحداث الدمار. يحدث التدمير عند
التداخل
عندما يقوم شخص ما برمي أو استخدام شيء ما ينشط العملية ، مثل كرة متدحرجة .
OnTakeDamage -
الكائن الذي يتم تدميره يتلف.
OnOverlapWithNearDestroyable
في هذه الحالة ، يتداخل أحد الكائنات القابلة للتدمير مع الآخر. في حالتنا ، من أجل البساطة ، كلاهما ينكسر.
تدفق تدمير الكائن
مخطط تدمير الكائن
عرض الأجزاء القابلة للتدمير
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); //
}
}
الوظيفة الرئيسية للتدمير
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
}
ماذا تفعل بوظيفة النوم
عندما يتم تشغيل وظيفة النوم ، نقوم بتعطيل الفيزياء / الاصطدامات وتعيين التنقل الثابت. سيؤدي ذلك إلى زيادة الإنتاجية.
يمكن لكل عنصر بدائي مع الفيزياء أن ينام. نحن نلتزم بهذه الوظيفة عند التدمير.
يمكن أن تكون هذه الوظيفة متأصلة في أي بدائية. نحن نلتزم به لإكمال العمل على الكائن.
في بعض الأحيان لا ينام الكائن المادي ويستمر في التحديث ، حتى لو لم ترَ أي حركة. إذا استمرت في محاكاة الفيزياء ، فإننا نجعل كل أجزائها تنام بعد 15 ثانية.
دعا الموقت وظيفة النوم القسري
void ADestroyable::OnPartPutToSleep(UPrimitiveComponent* InComp, FName InBoneName)
{
InComp->SetSimulatePhysics(false); //
InComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); //
InComp->SetMobility(EComponentMobility::Static); //
/* */
}
ماذا تفعل مع الدمار
نحتاج إلى التحقق مما إذا كان يمكن تدمير الممثل (على سبيل المثال ، إذا كان اللاعب بعيدًا). إذا لم يكن كذلك ، فسوف نتحقق مرة أخرى بعد مرور بعض الوقت.
دعنا نحاول تدمير الكائن في حالة عدم وجود اللاعب
void ADestroyable::DestroyAfterBreaking()
{
if (IsPlayerNear()) // ,
{
//
GetWorld()->GetTimerManager().SetTimer(DestroyAfterBreakTimerHandle, this, &ADestroyable::DestroyAfterBreaking, DESTROYACTORDELAY, false);
}
else
{
GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle); //
Destroy(); //
}
}
استدعاء عقدة OnHit لأجزاء من كائن
في حالتنا ، تعد Blueprints مسؤولة عن الجزء السمعي البصري من اللعبة ، لذلك نضيف أحداث Blueprints حيثما أمكن ذلك.
void ADestroyable::OnPartHitCallback(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit)
{
OnPartHitBP(Hit, NormalImpulse, HitComp, OtherComp); // blueprint
}
إنهاء التشغيل والتنظيف
يمكن لعب لعبتنا في المحرر الافتراضي وبعض المحررين المخصصين. لهذا السبب نحتاج إلى مسح كل ما في وسعنا في EndPlay.
void ADestroyable::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
/* */
GetWorld()->GetTimerManager().ClearTimer(DestroyAfterBreakTimerHandle);
GetWorld()->GetTimerManager().ClearTimer(ForceSleepTimerHandle);
Super::EndPlay(EndPlayReason);
}
التكوين في المخططات
التكوين بسيط هنا. يمكنك ببساطة وضع القطع المتصلة بالشبكة الأساسية وتمييزها على أنها "مقطوعة". هذا كل شئ. لا يحتاج فناني الجرافيك إلى فعل أي شيء في المحرك. تقوم فئة Blueprint الأساسية لدينا فقط بالأشياء السمعية والبصرية من الأحداث التي قدمناها في C ++. BeginPlay - يقوم بتنزيل الأصول المطلوبة. في الواقع ، في حالتنا ، كل أصل هو مؤشر لكائن برنامج ، وتحتاج إلى استخدامها حتى عند إنشاء نماذج أولية. ستؤدي مراجع الأصول المشفرة إلى زيادة أوقات تحميل المحرر / اللعبة واستخدام الذاكرة. عند الاستراحة - يستجيب للتأثيرات وأصوات المظهر. يمكنك العثور على بعض خيارات نياجرا هنا والتي سيتم وصفها لاحقًا. حدث الضربة الجزئية
- يطلق التأثيرات والأصوات.
أداة لإضافة التصادمات بسرعة
يمكنك استخدام Utility Blueprint للتفاعل مع الأصول لتوليد تصادمات لجميع أجزاء الكائن. إنها أسرع بكثير من إنشائها بنفسك.
تأثيرات الجسيمات في نياجرا
فيما يلي وصف لكيفية إنشاء تأثير بسيط في نياجرا .
مواد
مفتاح هذه المادة هو الملمس ، وليس التظليل ، لذا فهو حقًا بسيط للغاية.
التعرية واللون وألفا مأخوذة من نياجرا.
قناة Texture R قناة Texture
G
يتم تحقيق معظم التأثير عن طريق النسيج. لا يزال من الممكن استخدام القناة B لإضافة المزيد من التفاصيل ، لكننا لسنا بحاجة إليها في الوقت الحالي.
معلمات نظام نياجرا
نستخدم نظامين من نياجرا ، أحدهما لتأثير الانفجار (يستخدم شبكة أساسية لتفرخ الجسيمات) ، والآخر عندما تصطدم الأجزاء (لا يوجد ترتيب شبكي ثابت).
يمكن للمستخدم تحديد لون وعدد عمليات النشر وتحديد شبكة ثابتة سيتم استخدامها لتحديد موقع تفرخ الجسيمات
انفجر تفرخ نياجرا
هنا يتم إشراك المستخدم int32 حتى يتمكن من ضبط عداد المظهر لكل كائن قابل للتدمير
تفرخ جسيمات نياجرا
- اختيار شبكة ثابتة من الكائنات القابلة للتدمير ؛
- ضبط العمر والوزن والحجم بشكل عشوائي ؛
- اختر لونًا من الألوان المخصصة (يتم تعيينه بواسطة الممثل القابل للتدمير) ؛
- إنشاء جسيمات عند قمم الشبكة ،
- أضف سرعة عشوائية وسرعة دوران.
باستخدام شبكة ثابتة
لتتمكن من استخدام شبكة ثابتة في نياجرا ، يجب تحديد خانة الاختيار AllowCPU في شبكتك.
تلميح: في الإصدار الحالي (4.24) من المحرك ، إذا أعدت استيراد الشبكة ، فستتم إعادة تعيين هذه القيمة إلى القيمة الافتراضية. وفي بنية الشحن ، إذا حاولت تشغيل مثل هذا النظام من Niagara بشبكة لا تحتوي على إمكانية الوصول إلى وحدة المعالجة المركزية ، فسوف يتعطل.
لذلك دعونا نضيف بعض التعليمات البرمجية البسيطة للتحقق مما إذا كانت الشبكة مضبوطة على هذه القيمة.
bool UFunctionLibrary::MeshHaveCPUAccess(UStaticMesh* InMesh)
{
return InMesh->bAllowCPUAccess;
}
تم استخدامه في المخططات قبل نياجرا.
يمكنك إنشاء أداة محرر للعثور على الكائنات القابلة للتدمير وتعيين متغير Base Mesh على AllowCPUAccess.
إليك رمز Python الذي يبحث عن جميع الكائنات القابلة للتدمير ويضبط وصول وحدة المعالجة المركزية إلى الشبكة الأساسية.
كود Python لتعيين شبكة ثابتة 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
يمكنك تشغيله مباشرة باستخدام الأمر py ، أو إنشاء زر لتشغيل التعليمات البرمجية في الأداة المساعدة .
تحديث جسيمات نياجرا
عند التحديث ، نقوم بالأمور التالية:
- تحجيم ألفا على مدى الحياة ،
- أضف ضوضاء الضفيرة ،
- قم بتغيير سرعة الدوران وفقًا للتعبير: (Particles.RotRate * (0.8 - Particles.NormalizedAge) ؛
- مقياس حجم معلمة الجسيمات على مدى الحياة ،
- تحديث معلمة تمويه المواد ،
- أضف متجه ضوضاء.
لماذا مثل هذا النهج القديم إلى حد ما؟
بالطبع ، يمكنك استخدام نظام التدمير الحالي من UE4 ، ولكن بهذه الطريقة يمكنك التحكم بشكل أفضل في الأداء والمرئيات. عند سؤالك عما إذا كنت بحاجة إلى نظام كبير مثل النظام المدمج الذي يلبي احتياجاتك ، يجب أن تجد الإجابة بنفسك. لأن استخدامه في كثير من الأحيان غير معقول.
بالنسبة إلى Chaos ، دعنا ننتظر حتى يصبح جاهزًا لإصدار كامل ، ثم سنلقي نظرة على إمكانياته.