如何在虚幻引擎4和Blender中创建可破坏对象





现代游戏正在变得越来越现实,实现这一目标的一种方法是创建可破坏的环境。另外,砸家具,植物,墙壁,建筑物和整个城市都很好玩。



具有良好可破坏性的游戏最突出的例子是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,让我们等到它准备好完整发布后,再看一下它的功能。



All Articles