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

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

前回はWorldSettingsとGameModeの自作を行いました。

今回はここから、パドル、ボール、ブロックの順に作成していきたいと思います。

早速本題に入りましょう。

まずはパドルの実装からです。

パドルはPawnを継承し、各種操作を行うためにいろいろと設定を行っています。

そのため、ちょっと大きめになってしまっていますが、1つ1つしっかり提示していきたいと思います。

最初に.hだけ先に提示しておきます。

UCLASS()

class AMyPaddle : public APawn

{

    GENERATED_UCLASS_BODY()

    virtual void Tick(float DeltaSeconds) OVERRIDE;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Mesh)

    TSubobjectPtr<UStaticMeshComponent> MeshComp;

    UPROPERTY()

    class AMyBall* Ball;

    UFUNCTION(BlueprintCallable, Category="Pawn")

    virtual void MoveRight(float Val);

    UFUNCTION(BlueprintCallable, Category="Pawn")

    virtual void Shoot();

    UFUNCTION()

    void SetStaticMesh(UStaticMesh* pMesh);

private:

    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) OVERRIDE;

private:

    bool m_isBallShooted;

};

ちなみに、最初はActorを継承するようにしていたのですが、Pawnに変更しました。

継承元を変更するだけならソースコードをそのまま書き換えるだけで問題は発生しませんでした。

Tick()命令は1フレームに1回発行される命令です。

いわゆるUpdate命令ですね。

こちらはボールの生死のチェックとボールの新規作成を主な目的として実装しています。

MeshCompはStaticMeshのコンポーネントです。StaticMeshを表示するために必要です。

こちらはBlueprintで読み取ることが可能です。

Ballはプロパティになっていますが、BlueprintやLevel Editorからは見ることができません。

現在発射されているボールであり、初期値はNULL、つまり存在していません。

MoveRight()とShoot()は操作が行われた際の処理です。左右移動命令とボール発射命令です。

これらはBlueprintから呼び出すことが可能になっていますが、今回はBlueprintから呼び出すことはないです。

SetStaticMesh()はMeshCompにStaticMeshコンテンツを設定する命令ですが、MeshCompがプロパティ設定なので直接設定した方が楽だったと後で気が付きました。

ちなみに、MyBallクラスでもMeshCompにStaticMeshコンテンツを設定していますが、こちらはMeshCompに直接設定しています。

SetupPlayerInputComponent()は前回、PlayerPawnの変更でClientRestart()を利用した要因だったりします。

SetPawn()だとこの命令が呼び出されないのですが、ClientRestart()ならこの命令が呼び出されます。

この命令は入力に対する処理の設定を行います。つまり、マウスを移動したらパドルが動くとかですね。

m_isBallShootedは生成されたボールが発射されたかどうかのフラグです。

発射はマウス左ボタン、もしくはスペースバーで行うようにしているので、発射されたかどうかをチェックするフラグが必要でした。

では、.cppの実装を1つ1つ見ていきましょう。

まずはコンストラクタです。

AMyPaddle::AMyPaddle(const class FPostConstructInitializeProperties& PCIP)

    : Super(PCIP)

{

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

    RootComponent = MeshComp;

    Ball = NULL;

    PrimaryActorTick.bCanEverTick = true;

}

MeshCompの実体を作成しています。

CreateDefaultSubobject()命令で"PaddleMesh"という名前のStaticMeshComponentを作成しています。

このMeshCompはRootComponent、つまりコンポーネントツリーの根元に設定します。

Ballは発射しているボールの実体ですが、まだ存在しないのでNULLです。

PrimaryActorTick.bCanEverTickはtrueにしておきます。

これをtrueにしないとTick()命令が呼ばれません。

Tick()命令は毎フレーム1回呼び出されるので、更新の必要がないものは呼び出しをしないようにした方が高速に動作します。

例えば、ブロックはその場で静止していて、ボールがヒットしたときだけ処理が発生します。

このような場合はTick()命令を無駄に動かす必要はないので、この変数はfalseに設定しておきます。

デフォルトではfalseですので、明示的にTick()命令を呼び出したい場合に限りtrueにします。

さて、このコンストラクタではStaticMeshComponentを作成していますが、コンテンツを指定していません。

MyPaddleをLevel Editorで配置しようとすると、何も表示されないActorが配置されることになります。

もしも配置した段階でStaticMeshが表示されている状態にしたいのであればコンストラクタで設定する以外ありません。

コンテンツ名を直値指定すれば対応もできるのですが、MyWorldSettingsから持ってこようとしたら失敗しました。

ゲームの進行だけを考えると、この段階でMyWorldSettings::PaddleMeshを利用すればいい、ということになるのですが、Editorだと正しく動きません。

まあ、PaddleはC++側で生成するので、生成時にコンテンツを設定してやればOKです。

なお、配置オブジェクトに同様の処置を施す方法についてはブロックの際に解説します。

次にTick()命令です。

void AMyPaddle::Tick(float DeltaSeconds)

{

    Super::Tick(DeltaSeconds);

    if (Ball == NULL)

    {

        // ボールを新規に生成

        UWorld* world = GetWorld();

        AMyWorldSettings* settings = Cast<AMyWorldSettings>(world->GetWorldSettings());

        if (settings)

        {

            FVector location = GetActorLocation();

            location.Z += 50.0f;

            Ball = Cast<AMyBall>(world->SpawnActor(AMyBall::StaticClass(), &location));

            Ball->MeshComp->SetStaticMesh(settings->BallMesh);

            m_isBallShooted = false;

        }

    }

    else

    {

        // ボールが生きてるか確認する

        if (Ball->IsPendingKill())

        {

            Ball = NULL;

        }

    }

}

親クラスのTick()命令を実行した後に自前のTick()命令を処理します。

発射中のBallがNULLの場合、ボールを新規に作成します。

ボールの作成はパドルの作成と同様にSpawnActor()命令を利用します。

今回はPlayerControllerにPawnとして設定せず、自分自身で保持するようにしています。

出現場所はパドルの少し上ですが、ここでも直値を指定しているのはよろしくありませんね。

ボールを生成したらm_isBallShootedをfalseに設定します。

発射中のボールが存在する場合、このボールが生きているかどうかをチェックします。

ボールの死亡処理はパドルでは行っていませんが、死んだボールをそのまま保持しておくのは問題です。

AActor::IsPendingKill()命令は死亡したActorが死亡処理中かどうかをチェックすることができる命令です。

ActorはDestroy()命令が呼び出されると、死亡することが可能なら死亡処理に移行します。

Actorは死亡処理中に参照が完全に切れるとガベージコレクトで完全に削除される…らしいです。

ただ、スマートポインタなどを使用していない、生ポインタの状態でも参照を正しくチェックできているのかは疑問です。

その辺はちゃんとしているのかもしれませんが、保持しているActorが外から削除される可能性がある場合、保持しているクラスはきちんと生きているかチェックするようにした方が安全でしょう。

最後に操作関係の処理です。一気に見ていきましょう。

void InitializeDefaultPawnInputBindings()

{

    static bool bBindingsAdded = false;

    if (!bBindingsAdded)

    {

        bBindingsAdded = true;

        UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MoveRight", EKeys::A, -1.f));

        UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MoveRight", EKeys::D, 1.f));

        UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MoveRight", EKeys::Gamepad_LeftX, 1.f));

        UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("MoveRight", EKeys::MouseX, 1.f));

        UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("Shoot", EKeys::SpaceBar));

        UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("Shoot", EKeys::Gamepad_FaceButton_Bottom));

        UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("Shoot", EKeys::LeftMouseButton));

    }

}

void AMyPaddle::SetupPlayerInputComponent(UInputComponent* InputComponent)

{

    check(InputComponent);

    InitializeDefaultPawnInputBindings();

    InputComponent->BindAxis("MoveRight", this, &AMyPaddle::MoveRight);

    InputComponent->BindAction("Shoot", EInputEvent::IE_Pressed, this, &AMyPaddle::Shoot);

}

void AMyPaddle::MoveRight(float Val)

{

    FVector velocity(Val, 0.0f, 0.0f);

    FVector newLoc = GetActorLocation() + velocity * 10.0f;

    newLoc.Y = 0.0f;

    newLoc.Z = 80.0f;

    SetActorLocation(newLoc, true);

}

void AMyPaddle::Shoot()

{

    if (Ball != NULL && !m_isBallShooted)

    {

        m_isBallShooted = true;

        Ball->Speed = 5.0f;

        Ball->MoveDir = FVector(1.0f, 0.0f, 1.0f);

        Ball->MoveDir.Normalize();

    }

}

APlayerController::ClientRestart()を利用すると、PawnのSetupPlayerInputComponent()命令が呼び出されます。

この命令はある操作がされた際に呼び出される命令を設定する関数です。

この命令ではまず最初にInitializeDefaultPawnInputBindings()というローカル関数を呼び出しています。

この命令で使用されているUPlayerInput::AddEngineDefinedAxisMapping()とUPlayerInput::AddEngineDefinedActionMapping()ですが、[Edit] -> [Project Settings] -> [Engine - Input]のBindingsに対する設定をC++で行うための命令です。

キーバインドについてはUE WikiFPS作成チュートリアル辺りを参考にしてみてください。

もちろん、[Project Settings]であらかじめ設定しておいてもいいのですが、今回はC++側から指定してみました。

バインドしているのは左右移動の"MoveRight"とボール発射の"Shoot"のみです。

キーバインドを行った後、バインドされた名前と命令を関連付けているのがUInputComponent::BindAxis()とUInputComponent::BindAction()です。

それぞれに自分自身のMoveRight()命令とShoot()命令を関連付けています。

MoveRight()命令はパドルを移動するだけです。

パドル移動時に壁の外に出さないようにするため、SetActorLocation()命令の第2引数をtrueにしています。

bSweepというこの引数は設定前の位置から設定後の位置に移動する際の衝突判定をしっかりとってくれるようになります。

Shoot()命令はボールが発射状態にないなら斜め上方向にボールを発射します。

初速度なんかも直値です。よろしくありません。

今回はやってないのですが、ボールが発射状態にない場合にパドルの移動に合わせて移動するようにすることも可能です。

いわゆるブロック崩しの基本的な動作ですよね。

長くなってますがボールの実装に進みましょう。

やはり.hから提示します。

UCLASS()

class AMyBall : public AActor

{

    GENERATED_UCLASS_BODY()

    virtual void Tick(float DeltaSeconds) OVERRIDE;

    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Mesh)

    TSubobjectPtr<UStaticMeshComponent> MeshComp;

    UPROPERTY()

    float Speed;

    UPROPERTY()

    FVector MoveDir;

    UFUNCTION()

    void OnHit(class AActor* SelfActor, class AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit);

};

Tick()命令とMeshCompはパドルと同様です。

プロパティとしてSpeedとMoveDirが存在しますが、これはボールの速度と進行方向です。

重要なのがOnHit()命令です。

この命令はHitイベント発行時に呼び出されるデリゲートとして設定する必要があります。

.cppの最初はやっぱりコンストラクタですね。

AMyBall::AMyBall(const class FPostConstructInitializeProperties& PCIP)

    : Super(PCIP)

{

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

    MeshComp->GetBodyInstance()->bNotifyRigidBodyCollision = true;

    MeshComp->SetCollisionProfileName(TEXT("BlockAll"));

    RootComponent = MeshComp;

    Speed = 0.0f;

    MoveDir = FVector(0.0f, 0.0f, 0.0f);

    OnActorHit.AddDynamic(this, &AMyBall::OnHit);

    PrimaryActorTick.bCanEverTick = true;

}

MeshCompの生成後に2つの設定を行っています。

GetBodyInstance()->bNotifyRigidBodyCollisionはHitイベントの生成に必要なフラグのようです。

また、SetCollisionProfileName()でコリジョンプロファイルをBlockAll(すべてブロックする)に設定しています。

コリジョンプロファイルはデフォルトでいくつか存在していますが、自前で設定することも可能です。

今回はコリジョンプロファイルの追加は行っていませんが、追加する場合は.iniに直接書き込むかLevel Editorで指定できるようです。

C++から指定できるかどうかはわかりません。

各変数の初期化やTick()命令の有効化はパドルと同様です。

OnHit()命令をデリゲートとして設定してやる方法ですが、OnActorHit.AddDynamic()命令を使用します。

Hitイベントが発生するとOnActorHitに追加されている関数が呼び出されるようになっています。

AddDynamic()命令によってAMyBall::OnHit()を登録すると、Hitイベント発生時にきちんと呼ばれるようになります。

Tick()命令は非常に簡単です。

void AMyBall::Tick(float DeltaSeconds)

{

    Super::Tick(DeltaSeconds);

    // 移動する

    SetActorLocation(GetActorLocation() + MoveDir * Speed, true);

    // 一定以下の位置になったら自分を削除する

    if (GetActorLocation().Z < -100.0f)

    {

        Destroy();

    }

}

SetActorLocation()命令で位置を移動し、自分のZ座標が一定値以下になったら自分自身をDestroy()するだけです。

ボールのDestroy()はここで呼ばれるので、パドルは自分が保持しているボールクラスの実体が生きているかどうかをチェックする必要があったわけです。

次にOnHit()の中身です。

Hitイベントが発生した場合にボールが反射する、その処理を行っているだけですね。

void AMyBall::OnHit(class AActor* SelfActor, class AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit)

{

    FVector pos = GetActorLocation();

    pos.Y = 0.0f;

    SetActorLocation(pos);

    FVector NewDir = MoveDir.MirrorByVector(Hit.Normal);

    NewDir.Y = 0.0f;

    bool nonZero = NewDir.Normalize();

    if (!nonZero)

    {

        NewDir = -MoveDir;

    }

    MoveDir = NewDir;

    Speed += 0.1f;

    // ブロックに当たった場合の処理

    AMyBlock* pBlock = Cast<AMyBlock>(OtherActor);

    if (pBlock)

    {

        pBlock->Destruct();

    }

}

最初は、Hitした際に3次元的に移動してもらっても困るため、ActorのY座標を0.0にリセットしています。

次に衝突したものの法線方向からボールの反射方向を計算しています。

FHitResultには衝突時の情報が入ってきていて、Normalに衝突部分の法線方向が入っています。

これと前回の移動ベクトルから反射ベクトルをMirrorByVector()命令で求めています。

ありえないとは思いますが、反射ベクトルの長さが0.0になった場合は前回の移動ベクトルの逆ベクトルを新しい移動方向ベクトルとしています。

また、衝突時に速度を少し上げています。

最後は衝突したオブジェクトがブロックかどうかのチェックを行っています。

Castが成功したかどうかでそのオブジェクトが何かをチェックすることが可能です。

ブロックと衝突した場合、AMyBlock::Destruct()命令を呼び出すようにしていますが、この命令の詳細はブロックの説明で行います。

ソースコードばかり提示してたおかげで無駄に長くなってしまいました。

ブロックの処理については次回ということで。

その3へ続く…