C++でブロック崩し その3

[UE4] C++でブロック崩し その1

[UE4] C++でブロック崩し その2

予想通りに長くなってしまっていますが、今回で最後です。

最後はブロックの実装ですが、ここではUE4の機能である破壊可能メッシュ、DestructibleMeshも使っています。

プログラマでもこの部分については参考になるのではないかと思います。

DestructibleMeshについてはUE4.0のバグを若干踏んでいたのですが、UE4.1では修正されています。

もしも同じような部分で困っている方がいましたらすぐにUE4.1にアップデートしましょう。

では先に進んでいきます。

まずはMyBlockクラスの.hを見ていきましょう。

UCLASS()

class AMyBlock : public AActor

{

    GENERATED_UCLASS_BODY()

    virtual void BeginPlay() OVERRIDE;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Mesh)

    TSubobjectPtr<UStaticMeshComponent> MeshComp;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Mesh)

    TSubobjectPtr<UDestructibleComponent> DestructComp;

    UFUNCTION()

    void Destruct();

};

まず目に付くのはDestructCompでしょう。

UE4ではStaticMeshを破壊可能な状態、つまり、破片ごとに分割し、それを合わせてStaticMeshを表現したDestructibleMeshというものが存在しています。

このコンテンツを利用するためのコンポーネントがUDestructibleComponentです。

なお、DestructibleMeshはSkeletalMeshを継承しているクラスなので、DestructibleMeshはSkeletalMeshComponentでも使用することが可能です。

ただ、破壊して動かすようなことはできないんじゃないかと思います。

DestructCompと一緒にStaticMeshを利用するMeshCompも宣言されています。

UE4.0.2の段階ではどうやらDestructibleComponentと移動体の衝突判定が正常に行われていないようでした。

なのでDestructibleComponentだけだとブロックの役割を果たしてくれませんでした。

苦肉の策として、壊れるまではStaticMeshを表示し、壊れたら(ボールがヒットしたら)DestructibleMeshを表示して破壊する、という手段をとりました。

BeginPlay()では最初にMeshCompだけを表示し、DestructCompを非表示するようにしています。

Destruct()関数は前回、AMyBall::OnHit()関数で呼び出されていた命令です。

この命令はMeshCompを非表示にし、DestructCompを表示、破壊するための命令です。

今回は耐久力を設定していませんが、耐久力を設定するのであればこの部分で耐久力を減らす処理をするとよいかと思います。

次に.cppを見ていきましょう。コンストラクタは以下の通りです。

AMyBlock::AMyBlock(const class FPostConstructInitializeProperties& PCIP)

    : Super(PCIP)

{

    MeshComp = PCIP.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("BlockMesh"));

    RootComponent = MeshComp;

    DestructComp = PCIP.CreateDefaultSubobject<UDestructibleComponent>(this, TEXT("DestructMesh"));

    DestructComp->SetCollisionProfileName("NoCollision");

    DestructComp->AttachParent = MeshComp;

}

StaticMeshComponentを作成し、これをルートコンポーネントとして設定しています。

この後、DestructibleComponentを作成し、MeshCompの子として設定しています。

また、コリジョンプロファイルを"NoCollision"に設定しています。

この設定を行わないと、破壊後の破片がボールやパドルに接触してしまいます。

ボールに当たるのはゲーム的なランダム要素として許容できるかもしれませんが、パドルに破片が乗ってしまうとパドルの移動ができなくなりました。

これはさすがにまずいのでコリジョンなしにした次第です。

コリジョンプロファイルは新規に作成することも可能なので、例えばボールには当たるけどパドルには当たらない、といったプロファイルも作成は可能です。

興味がある方はそんなプロファイルを作ってみるのもいいかもしれませんね。

BeginPlay()は単純です。

void AMyBlock::BeginPlay()

{

    Super::BeginPlay();

    MeshComp->SetVisibility(true);

    DestructComp->SetVisibility(false);

}

MeshCompを表示状態に、DestructCompを非表示状態にしているだけです。

最後はDestruct()命令の実装です。

void AMyBlock::Destruct()

{

    MeshComp->SetVisibility(false);

    MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);

    DestructComp->SetVisibility(true);

    DestructComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);

    DestructComp->SetSimulatePhysics(true);

    DestructComp->WakeAllRigidBodies();

    DestructComp->ApplyRadiusDamage(1000.0f, GetActorLocation(), 100.0f, 500.0f, true);

}

最初にMeshCompを非表示に、また、コリジョンをなしに設定します。

DestructCompは表示に、また、コリジョンなしにしています。

しかし、MeshCompについてはSetCollisionEnabled()命令が正常に効いていたのですが、DestructCompはSetCollisionEnabled()を利用してもコリジョンが発生したままでした。

これが不具合なのかどうかは不明です。

最後の3行はDestructCompの物理挙動についての設定です。

SetSimulatePhysics()で物理挙動を有効にし、WakeAllRigidBodies()で止まっていた物理挙動を”起して”やります。

これだけだとDestructCompが重力に従って落下するだけです。破片が飛び散りません。

最後のApplyRadiusDamage()命令でDestructCompに致命的なダメージを与え、破壊するようにしています。

この命令は指定のワールド座標から指定半径分の範囲にダメージを与えます。

ダメージが一定量以上になるとDestructibleComponentは破壊されるようです。

必要なダメージ量などはEditorで設定が可能ですが、どの設定がどういう意味を持っているのかはまだ詳しく調べていません。

ブロックはパドルやボールと違ってLevel Editorで配置されなければいけません。

自作のActorを配置するには[Modes]タブの[Place] -> [All Classes]から見つけてくるか、上の検索窓を使って検索するかで見つかるので、これをドラッグ&ドロップして配置しましょう。

ue049.png

しかし、配置した段階ではStaticMeshもDestructibleMeshも設定されていないため、移動させるための軸だけが表示されているはずです。

そこで[Details]タブをチェックしましょう。StaticMeshとDestructibleMeshを設定する部分が存在しているので、そこに適切なコンテンツを設定します。

ue050.png 

DestructibleMeshはどうやって作成すればよいのでしょうか?

まず、コンテンツブラウザのStaticMeshを右クリックしてみてください。

ue051.png

赤で示した部分に[Create Destructible Mesh]というコマンドがありますので、こちらをクリックしてください。

するとDestructibleMeshが生成され、以下のようなウィンドウが表示されます。

ue052.png 

このウィンドウで破壊した場合のオブジェクトを確認することが可能です。

赤で囲った[Fructure Mesh]ボタンを押すと現在の設定でメッシュの分割が行われます。

黄色で囲った[Explode Amount]をスライドさせると破片がどのように分裂しているかわかりやすいでしょう。

緑の[Fructure Settings]タブでは破片の数やランダムの種を設定することができます。

破片が多すぎると物理挙動が重くなることも予想されるので、自身が作成するゲームに合わせて数を指定してください。

なお、UE4.0.2ではBrushから作成したStaticMeshからDestructibleMeshを作成しようとするとEditorがクラッシュします。

UE4.1では修正されているので、アップデートしていない方は速やかにアップデートしてください。

ここまでのコードをビルドして各Actorの配置を行っていればあとはゲームが遊べるようになっているはずです。

もちろん、まだこの段階では公開できるゲームにはなっていないのですが、とりあえず動くことは動きます。

さて、C++でゲームを作ってきましたが、Blueprintですらやってこなかったことをいろいろやったために結構苦労しました。

不具合を踏んだのも時間がかかった理由ではありますが、一部の不具合についてはソースコードが公開されていることで自前の修正も可能だったりしたのがよかったですね。

とはいえ、これはソースコードが読めるプログラマの特権であり、非プログラマにはあまり有利に働かないでしょう。

また、これは結構重要なことなのですが、C++で作成したActorのコンポーネント変更はかなり難しいです。

例えば、今回の場合、ブロックをStaticMeshからSkeletalMeshにしたい、ということになったとします。

ここで、TSubobjectPtr<UStaticMesh> MeshComp; を TSubobjectPtr<USkeletalMesh> MeshComp; に変更したとしましょう。

既にブロックが配置されている状態でこの変更を行うと、Editorを立ち上げる際に致命的なエラーが出てクラッシュしてしまいます。

.umapにはMeshCompはStaticMeshで作成します、と書かれているのに、実際のActorではMeshCompはSkeletalMeshなので作成できません、となってクラッシュするようです。

回避策としては、同名のコンポーネントにしない、という手があります。

削除されている分にはどうやら問題ないようなので、削除したコンポーネントと別名にしておけばクラッシュは防ぐことができます。

しかし、クラッシュを防ぐことはできてもすでに配置されているブロックのSkeletalMeshは再設定しなければいけません。

これを賢く回避するのであれば、そもそもMyBlockを生で使用するのではなく、BPに継承させて使用する方がよっぽどいいのです。

ブロックの種類分だけBPが存在していますが、すべてのブロックの設定を変更するよりよっぽど簡単で管理もしやすいでしょう。

C++の弱点はまだまだあって、Level固有の処理やActorの管理はとにかく苦手です。

カメラの設定の際にタグを利用していましたが、LevelBPを利用すればタグを利用することなく対応ができたわけです。

他にも、Level内のある特定の何かをどうする、というような場合に何らかの方法でLevel中のActorを検索しなければならず、その際にタグなりなんなりで対応しなければいけないわけです。

Level固有のActorを利用する場合は、BPのみで対応するか、もしくはBP側からC++の関数を呼び出し、その際にActorを指定するやり方をとるべきでしょう。

しかし、C++はBPに比べるとやはり高速なはずです。

BPはスクリプトであるので、特に重い計算が走る処理やループが多い処理ではコンパイラが最適化したC++にはかなわないでしょう。

また、計算式が複雑になるような場合は要注意です。

BPでは計算式の評価はその値が使用される際に行われているようなので、C++でいうところのローカル変数に保存した情報を利用する際に思わぬ挙動をすることがあります。

例えば以下のような状況です。

int v = getTestValue() + 1;

setTestValue( v );

if (v == 100)

{

    // 何か処理する

}

これをBPで表現するとこのようになると思います。

ue053.png 

私が試した限りでは、TestValueに+1された値との比較ではなく、TestValueに+2された値との比較になりました。

プログラマとしてはこのような処理は直感的とは言えないです。

そういう点で見ると、特に計算処理が複雑になったり、その間で分岐が多くなったりするようならそこの処理だけでもC++に逃がしてやるべきかと思います。

BPから呼び出せる関数にしておけば苦労せずに利用できるはずです。

最後に、C++の利用方法を個人的に考えている部分で書いておきます。

1.Actorの型を特定するためにC++で新しいActorを作るのは有効

  ゲームに特化したActorのテンプレート的に用いる

  ただし、Level Editorで配置する場合はBlueprintに継承させて使う方がよい

2.ループが多い処理、計算が複雑な処理はC++側に逃がす

  最初はBPで作成しておいて、あとでC++に移動させるとやりやすそう

3.C++で特定のActorを利用する場合、できるだけC++側で生成から破棄まで管理する

  どうしてもLevel Editorで配置したActorを利用したい場合、レベルデザイナさんに特定の関数を呼んでもらって管理を移譲する

4.処理コストに問題ないならBPを使いましょう

  無理にC++にする必要はありません

と、こんな感じでしょうか。

しかしまあ、最終的には処理コストの面で結構C++を使うことになるんじゃないかと思ったりもしますね…