- PVSM.RU - https://www.pvsm.ru -
Современные игры становятся все реалистичнее, и один из способов этого добиться — создать разрушаемое окружение. К тому же, крушить мебель, растения, стены, здания и целые города просто весело.
Наиболее яркими примерами игр с хорошей разрушаемостью можно назвать Red Fraction: Guerrilla с ее возможностью пробивать тоннель сквозь Марс, Battlefield: Bad Company 2, где при желании можно превратить весь сервер в пепелище, и Control с его процедурным разрушением всего, что попадается на глаза.
В 2019 году Epic Games представила демо новой высокопроизводительной системы физики и разрушений Chaos [1] движка Unreal. Новая система позволяет создавать разрушения разного масштаба, имеет поддержку редактора эффектов Niagara и при этом отличается экономным расходованием ресурсов.
А пока Chaos находится на стадии бета-тестирования, поговорим об альтернативных подходах к созданию разрушаемых объектов в Unreal Engine 4. В этой статье один из них опишем подробно.
Начнем с перечисления того, чего мы хотели бы достичь:
За референс в этой статье было взято разрушаемое окружение из Dark Souls 3 и Bloodborne.
На самом деле, идея проста:
Для подготовки объектов будем использовать Blender. Для создания сетки, по которой они будут разрушаться, используем аддон Blender под названием Cell Fracture.
Сначала нам понадобится включить аддон, поскольку по умолчанию он выключен.
[2]
Включение аддона Cell Fracture
Затем включаем аддон на выбранной сетке.
Посмотрите видео, сверьтесь с настройками оттуда. Убедитесь, что вы правильно настроили свои материалы.
Затем создадим UV-карту для этих частей.
Edge Split исправит затенение.
Их использование позволит применить Edge Split ко всем выбранным частям.
Вот как это выглядит в Blender. По сути, нам не нужно моделировать все части по отдельности.
Наш разрушаемый объект — это Актер, у которого есть несколько компонентов:
Изменим кое-какие настройки в конструкторе:
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 мы собираем некоторые данные и настраиваем их:
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;
}
Существует три способа спровоцировать разрушение.
OnOverlap
Разрушение происходит в том случае, когда кто-то бросает или каким-либо другим образом использует предмет, активирующий процесс, — например, катящийся мяч.
OnTakeDamage
Разрушаемый объект получает урон.
OnOverlapWithNearDestroyable
В этом случае один разрушаемый объект накладывается на другой. В нашем случае для простоты они оба ломаются.
[9]
Диаграмма разрушения объекта
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 отвечает за аудиовизуальные эффекты
}
Когда срабатывает функция Sleep, мы отключаем физику/столкновения и устанавливаем статичную мобильность. Благодаря этому производительность увеличится.
Каждый примитивный компонент с физикой может перейти в режим сна. Привязываемся к этой функции при разрушении.
Эта функция может быть присуща любому примитиву. Мы привязываемся к ней для завершения действия над объектом.
Иногда физический объект не переходит в режим сна и продолжает обновляться, даже если вы не видите при этом никакого движения. Если он продолжает моделировать физику, мы заставляем все его части перейти в режим сна спустя 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(); // разрушение актера сцены
}
}
В нашем случае 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);
}
Конфигурация здесь проста. Вы просто помещаете части, прикрепленные к базовой сетке, и помечаете их как «dest». Вот и все.
Графическим художникам не нужно ничего делать в движке.
Наш базовый класс Blueprint выполняет только аудиовизуальные вещи из событий, которые мы предоставили на C ++.
BeginPlay — загружает необходимые ассеты. По сути, в нашем случае каждый ассет представляет собой указатель на программный объект, и необходимо использовать их даже при создании прототипов. Жестко запрограммированные референсы ассетов увеличат время загрузки редактора/игры и использование памяти.
On Break Event — отвечает на эффекты и звуки появления. Здесь вы можете найти некоторые параметры Niagara, которые будут описаны позже.
On Part Hit Event — вызывает эффекты и звуки ударов.
Можно использовать Utility Blueprint [13] для взаимодействия с ассетами, чтобы генерировать коллизии для всех частей объекта. Это намного быстрее, чем создавать их самостоятельно.
Далее описывается создание простого эффекта в Niagara [16].
Ключевой в этом материале является текстура, а не шейдер, так что он действительно очень простой.
Эрозия, цвет и альфа берутся из Niagara.
Канал текстуры R
Канал текстуры G
Большая часть эффекта достигается текстурой. Можно было бы еще использовать канал B, чтобы добавить больше деталей, но в настоящее время нам он не нужен.
Мы используем две системы Niagara: одну для эффекта разрыва (она использует базовую сетку для порождения частиц), а другую — при столкновении деталей (без статичного расположения сетки).
Пользователь может указать цвет и количество спаунов и выбрать статичную сетку, которая будет использоваться для выбора расположения спауна частиц
Здесь задействуется пользователь int32 для того, чтобы иметь возможность настроить счетчик появления для каждого разрушаемого объекта
Чтобы иметь возможность использовать статичную сетку в Niagara, на вашей сетке должен быть установлен флажок AllowCPU.
СОВЕТ: В текущей (4.24) версии движка, если вы повторно импортируете свою сетку, это значение будет сброшено на значение по умолчанию. А в доставочной сборке, если вы попытаетесь запустить такую систему Niagara с сеткой, у которой не включен доступ к ЦП, произойдет сбой.
Поэтому добавим простой код для проверки, установлено у сетки это значение.
bool UFunctionLibrary::MeshHaveCPUAccess(UStaticMesh* InMesh)
{
return InMesh->bAllowCPUAccess;
}
Он используется в Blueprints до Niagara.
Можно создать виджет редактора для поиска разрушаемых объектов и установить их переменную Base Mesh AllowCPUAccess.
Приведем код на Python, который ищет все разрушаемые объекты и устанавливает доступ ЦП к базовой сетке.
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 [19] или создать кнопку для запуска кода в Utility Widget [20].
При обновлении мы проделываем следующие вещи:
Конечно, можно использовать текущую систему разрушений из UE4, но так можно эффективнее контролировать производительность и визуальные эффекты. На вопрос, нужна ли для ваших нужд столь большая система, как встроенная по умолчанию, вы должны найти ответ сами. Потому что часто ее использование бывает необоснованным.
Что же касается Chaos, подождем, когда он будет готов к полноценному релизу, и тогда посмотрим на его возможности.
Автор: Pixonic
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/gamedev/356607
Ссылки в тексте:
[1] Chaos: https://docs.unrealengine.com/en-US/Engine/Chaos/ChaosDestruction/index.html
[2] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/EnableAddon.jpg
[3] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/SearchForCellFractureAddon.jpg
[4] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/CellFractureSetings.jpg
[5] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/SelectByMaterial.jpg
[6] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/UnwrapCutedParts.jpg
[7] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/AddEdgeSplitModif.jpg
[8] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/CellFracture_final.jpg
[9] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-4.png
[10] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-5.png
[11] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-7.png
[12] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-8.png
[13] Utility Blueprint: https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Blueprints/ScriptedActions/index.html
[14] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-27.png
[15] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-26.png
[16] Niagara: https://docs.unrealengine.com/en-US/Engine/Niagara/index.html
[17] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-11.png
[18] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-17.png
[19] py: https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Python/index.html
[20] Utility Widget: https://docs.unrealengine.com/en-US/Engine/UMG/UserGuide/EditorUtilityWidgets/index.html
[21] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-25.png
[22] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image22.png
[23] Image: https://kidswithsticks.com/wp-content/uploads/2020/08/image-23.png
[24] Источник: https://habr.com/ru/post/517684/?utm_source=habrahabr&utm_medium=rss&utm_campaign=517684
Нажмите здесь для печати.