現代のゲームはより現実的になりつつあり、これを達成する1つの方法は、破壊可能な環境を作成することです。さらに、家具、植物、壁、建物、そして都市全体を壊すのはとても楽しいです。
優れた破壊性を備えたゲームの最も印象的な例は、火星をトンネルで貫通する能力を備えたRed Fraction:Guerrilla、必要に応じてサーバー全体を灰に変えることができるBattlefield:Bad Company 2、そして目を引くすべてのものを手続き的に破壊するControlです。
2019年、Epic Gamesは、Unrealの新しい高性能物理および破壊システムであるChaosのデモを発表しました。新しいシステムでは、さまざまなスケールの破壊を作成でき、ナイアガラエフェクトエディターをサポートしていると同時に、リソースの経済的な使用が特徴です。
それまでの間、Chaosはベータテスト中です。UnrealEngine4で破壊可能なオブジェクトを作成するための代替アプローチについて説明しましょう。この記事では、そのうちの1つについて詳しく説明します。
要件
達成したいことをリストすることから始めましょう:
- 芸術的なコントロール。私たちは、アーティストが好きなように破壊可能なオブジェクトを作成できるようにしたいと考えています。
- ゲームプレイに影響を与えない破壊。それらは純粋に視覚的であり、ゲームプレイに関連するものを邪魔しないものでなければなりません。
- 最適化。パフォーマンスを完全に制御し、CPUをダウンさせないようにする必要があります。
- インストールが簡単。このようなオブジェクトの構成の設定は、アーティストが理解できるものである必要があるため、必要最小限の手順のみを含める必要があります。
この記事では、Dark Souls3とBloodborneの破壊可能な環境を参照として取り上げました。
本旨
実際、アイデアは単純です。
- 目に見えるベースラインメッシュを作成します。
- メッシュの非表示部分を追加します。
- 破壊時:ベースメッシュを非表示->そのパーツを表示->物理を開始します。
資産の準備
Blenderを使用してオブジェクトを準備します。崩壊するメッシュを作成するには、CellFractureと呼ばれるBlenderアドオンを使用します。
アドオンの有効化
アドオンはデフォルトで無効になっているため、最初に有効にする必要があります。CellFractureアドオンの有効化
検索アドオン(F3)
次に、選択したグリッドでアドオンを有効にします。
構成設定
アドオンの起動
ビデオを見て、そこから設定を確認してください。材料が正しく設定されていることを確認してください。
カットピースを展開するための材料の選択
次に、これらのパーツのUVマップを作成します。
エッジスプリットの追加
エッジスプリットはシェーディングを修正します。
リンク修飾子
それらを使用すると、選択したすべてのパーツにエッジ分割が適用されます。
完了
これが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では、いくつかのデータを収集してカスタマイズします。
- 「dest」タグが付いているすべてのパーツを探します。
- アーティストがそれについて考える必要がないように、すべての部分に衝突を設定します。
- 静的モビリティを確立します。
- すべてのパーツを非表示にします。
BeginPlayでオブジェクトの一部を設定する
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;
}
破壊トリガー
破壊を誘発する方法は3つあります。
OnOverlap
Destructionは、ローリングボールなど、プロセスをアクティブにするオブジェクトを誰かが投げたり、使用したりしたときに発生します。
OnTakeDamage
破壊されているオブジェクトがダメージを受けます。
OnOverlapWithNearDestroyable
この場合、破壊可能な1つのオブジェクトが別のオブジェクトとオーバーラップします。私たちの場合、簡単にするために、両方とも壊れています。
オブジェクト破壊フロー
オブジェクト破壊図
破壊可能な部品を表示する
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-必要なアセットをダウンロードします。実際、私たちの場合、各アセットはプログラムオブジェクトへのポインタであり、プロトタイプを作成する場合でもそれらを使用する必要があります。ハードコーディングされたアセット参照により、エディター/ゲームのロード時間とメモリ使用量が増加します。休憩イベント時-エフェクトと外観サウンドに応答します。後で説明するいくつかのナイアガラオプションをここで見つけることができます。パートヒットイベントについて
-インパクトエフェクトとサウンドをトリガーします。
衝突をすばやく追加するためのユーティリティ
ユーティリティブループリントを 使用してアセットを操作し、オブジェクトのすべての部分の衝突を生成できます。自分で作成するよりもはるかに高速です。
ナイアガラの粒子効果
以下では、ナイアガラで簡単な効果を作成する方法について説明します。
素材
この素材の鍵はシェーダーではなくテクスチャーなので、とてもシンプルです。
侵食、色、アルファはナイアガラから取られています。
テクスチャチャネルRテクスチャ
チャネルG
ほとんどの効果はテクスチャによって実現されます。運河Bを使用して詳細を追加することもできますが、現時点では必要ありません。
ナイアガラシステムパラメータ
2つのナイアガラシステムを使用します。1つはバースト効果(ベースメッシュを使用して粒子を生成する)用で、もう1つはパーツが衝突するとき(静的メッシュ位置なし)です。
ユーザーはスポーンの色と数を指定し、パーティクルスポーンの場所を選択するために使用される静的メッシュを選択できます。
ナイアガラスポーンバースト
ここでは、破壊可能な各オブジェクトの外観カウンターを調整できるようにするために、ユーザーint32が関与しています。
ナイアガラパーティクルスポーン
- 破壊可能なオブジェクトから静的メッシュを選択する。
- ランダムな寿命、重量、サイズを設定します。
- カスタムカラーから色を選択します(破壊可能なアクターによって設定されます)。
- メッシュの頂点に粒子を作成し、
- ランダム速度と回転速度を追加します。
静的グリッドの使用
Niagaraで静的メッシュを使用できるようにするには、メッシュで[AllowCPU]チェックボックスをオンにする必要があります。
ヒント:現在の(4.24)バージョンのエンジンでは、メッシュを再インポートすると、この値はデフォルトにリセットされます。また、出荷ビルドでは、CPUアクセスが有効になっていないメッシュでこのようなナイアガラシステムを実行しようとすると、クラッシュします。
それでは、グリッドがこの値に設定されているかどうかを確認するための簡単なコードを追加しましょう。
bool UFunctionLibrary::MeshHaveCPUAccess(UStaticMesh* InMesh)
{
return InMesh->bAllowCPUAccess;
}
ナイアガラ以前のブループリントで使用されていました。
破壊可能なオブジェクトを見つけるためのエディターウィジェットを作成し、それらのベースメッシュ変数をAllowCPUAccessに設定できます。
これは、すべての破壊可能なオブジェクトを検索し、基になるメッシュへのCPUアクセスを設定するPythonコードです。
静的グリッドallow_cpu_access変数を設定する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 コマンドを使用して直接実行するか、ユーティリティウィジェットでコードを実行するためのボタンを作成できます。
ナイアガラパーティクルアップデート
更新するときは、次のことを行います。
- アルファオーバーライフのスケーリング、
- カールノイズを追加し、
- 次の式に従って回転速度を変更します。(Particles.RotRate *(0.8-Particles.NormalizedAge) ;
- Size OverLifeパーティクルパラメータをスケーリングします。
- マテリアルブラーパラメータを更新し、
- ノイズベクトルを追加します。
なぜそのようなかなり古い学校のアプローチ?
もちろん、UE4の現在の破壊システムを使用することもできますが、このようにして、パフォーマンスとビジュアルをより適切に制御できます。必要に応じて組み込みシステムと同じ大きさのシステムが必要かどうかを尋ねられたら、自分で答えを見つける必要があります。その使用はしばしば不合理だからです。
カオスについては、本格的なリリースの準備が整うまで待ってから、その機能を見ていきましょう。