现代游戏正在变得越来越现实,实现这一目标的一种方法是创建可破坏的环境。另外,砸家具,植物,墙壁,建筑物和整个城市都很好玩。
具有良好可破坏性的游戏最突出的例子是Red Fraction:游击队,具有穿越火星的能力; Battlefield:Bad Company 2,您可以根据需要将整个服务器变成灰烬;其程序破坏了所有吸引您眼球的东西。
在2019年,Epic Games展示了虚幻的新型高性能物理和破坏系统Chaos的演示。新系统允许您创建不同比例的破坏,支持Niagara效果编辑器,同时还以节省资源为特色。
同时,Chaos正在Beta测试中,让我们讨论在虚幻引擎4中创建可破坏对象的替代方法。在本文中,我们将详细介绍其中一种。
要求
让我们首先列出我们要实现的目标:
- 艺术控制。我们希望我们的艺术家能够根据需要创建可破坏的对象。
- 不影响游戏玩法的破坏。它们应该纯粹是视觉的,不会干扰任何与游戏有关的内容。
- 优化。我们希望完全控制性能,而不是让CPU宕机。
- 易于安装。设置这些对象的配置对于美术师应该是可以理解的,因此有必要仅包括必要的最少步骤。
本文采用了《黑暗之魂3》和《血源》的可破坏环境作为参考。
大意
实际上,这个想法很简单:
- 创建一个可见的基线网格;
- 添加网格的隐藏部分;
- 关于破坏:隐藏基础网格->显示其部分->开始物理。
准备资产
我们将使用Blender准备对象。为了创建沿其折叠的网格,我们使用了一个称为Cell Fracture的Blender附加组件。
启用插件
首先,我们需要启用插件,因为默认情况下它是禁用的。启用细胞骨折插件
搜索插件(F3)
然后在所选网格上启用插件。
配置设定
启动插件
观看视频,从那里检查设置。确保正确设置材料。
展开切割件的材料选择
然后,我们将为这些零件创建UV贴图。
添加边缘分割
边缘分割将修复阴影。
链接修饰符
使用它们会将“边缘分割”应用到所有选定的零件。
完成时间
这就是在Blender中的外观。基本上,我们不需要对所有零件分别建模。
实作
基类
我们的可破坏对象是Actor,它具有多个组件:
- 根场景;
- 静态网格物体-基础网格物体;
- 碰撞盒
- 地板盒;
- 径向力。
让我们在构造函数中更改一些设置:
- 禁用滴答计时器功能(不要忘记为不需要它的演员禁用它);
- 我们为所有组件设置了静态移动性;
- 禁用对导航的影响;
- 配置冲突配置文件。
在构造函数中设置一个actor
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;
}
破坏触发器
有三种引发破坏的方式。 当有人扔出或以其他方式使用可激活过程的对象(例如
滚球)
时,就会发生OnOverlap破坏。
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节点
在我们的案例中,蓝图负责游戏的视听部分,因此我们尽可能添加蓝图事件。
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-下载所需的资产。实际上,在我们的案例中,每个资产都是指向程序对象的指针,即使在创建原型时也需要使用它们。硬编码的资产引用将增加编辑器/游戏的加载时间和内存使用量。休息事件-响应效果和外观声音。您可以在此处找到一些Niagara选项,稍后将进行介绍。部分击中事件
-触发冲击效果和声音。
快速添加碰撞的工具
您可以使用实用程序蓝图与资产进行交互,以为对象的所有部分生成碰撞。这比自己创建它们要快得多。
尼亚加拉的粒子效应
下面介绍如何在Niagara中创建简单的效果。
材料
这种材料的关键是纹理,而不是着色器,因此它非常简单。
侵蚀,颜色和alpha取自尼亚加拉。
纹理通道R纹理
通道G
大多数效果是通过纹理实现的。运河B仍可用于添加更多细节,但我们目前不需要。
Niagara系统参数
我们使用两个Niagara系统:一个用于爆发效果(它使用基础网格物体生成粒子),另一个用于零件碰撞时(没有静态网格物体位置)。
用户可以指定生成的颜色和数量,并选择一个静态网格物体,用于选择粒子生成的位置
尼亚加拉产卵爆发
这里涉及到用户int32以便能够为每个可破坏对象调整外观计数器
尼亚加拉粒子繁殖
- 从可破坏对象中选择静态网格物体;
- 设置随机的寿命,重量和大小;
- 从自定义颜色中选择一种颜色(由可破坏角色设置);
- 在网格顶点处创建粒子,
- 添加随机速度和旋转速度。
使用静态网格
为了能够在Niagara中使用静态网格物体,您的网格物体必须选中“ AllowCPU”复选框。
提示:在当前(4.24)版本的引擎中,如果重新导入网格,则该值将重置为默认值。在运输版本中,如果您尝试使用未启用CPU访问权限的网格运行这样的Niagara系统,它将崩溃。
因此,让我们添加一些简单的代码来检查网格是否设置为此值。
bool UFunctionLibrary::MeshHaveCPUAccess(UStaticMesh* InMesh)
{
return InMesh->bAllowCPUAccess;
}
在尼亚加拉之前,它已用于蓝图。
您可以创建一个编辑器小部件来查找可破坏的对象,并将其“基本网格”变量设置为AllowCPUAccess。
这是一个Python代码,该代码查找所有可破坏的对象并设置对底层网格的CPU访问权限。
设置静态网格的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命令运行它,或者创建一个按钮以运行Utility Widget中的代码。
尼亚加拉粒子更新
更新时,我们执行以下操作:
- 在生命中扩展Alpha,
- 添加卷曲噪音,
- 根据以下表达式更改旋转速度:(Particles.RotRate *(0.8-Particles.NormalizedAge) ;
- 缩放“生命周期大小”粒子参数,
- 更新材质模糊参数,
- 添加噪声矢量。
为什么采用这样的老式方法?
当然,您可以使用UE4中的当前销毁系统,但是通过这种方式,您可以更好地控制性能和视觉效果。当系统询问您是否需要与内置系统一样大的系统时,您必须自己找到答案。因为它的使用常常是不合理的。
至于Chaos,让我们等到它准备好完整发布后,再看一下它的功能。