[UE4/UE5] GASでオンラインゲーム Part4 死と再生の実装

Post 2022年2月26日土曜日

C++ GameplayAbility GASでオンラインシリーズ UE4 UE5 Unreal Engine オンラインゲーム

Part 1の一部になります。

前回の続き

今回はGameplay Ability System (通称GAS)を使って、HPが0になったら死ぬを実装します。

お約束
この記事作成にあたって使用した主なUnreal Engine バージョンUE4.27.2 / UE5 Preview1

本日のゴール

HPが0になったら死んでリスポーンを実装します。


参考文献

https://github.com/Pantong51/GASContent/blob/master/Tutorial_Attribute_Delegates.md

https://github.com/tranek/GASDocumentation/blob/master/README.md#412-setup-and-initialization


第一段階:HPが0になった時に発動するイベントを作る

雪玉が当たった時にGameplay Effect (GE) でHPを減らし、HPが0になったら死ぬを実装します。

まずキャラクター.hにいきまして、#includeの下にこれを宣言します

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FHealthChange, float, CharacterHealth);

Delegateの宣言ですね。


続いてpublic:の下にこれを加えます。

    UFUNCTION(BlueprintNativeEvent, Category= "GameplayAbility|Die")

    void Die();

    virtual void Die_Implementation();

    

    UPROPERTY(BlueprintAssignable, Category = "Delegates")

    FHealthChange OnHealthChange;

    

    UFUNCTION(BlueprintCallable, Category = "GameplayAbility|PlayerState")

    bool IsAlive() const;


    UFUNCTION(BlueprintCallable, Category = "GameplayAbility|Cancel")

    virtual void CancelCharacterAllAbilities();

死んだ時に発生するイベントと、死の終わりの関数、先程のDelegateのインスタンス、生きてるかどうかの確認関数ですね。最後のCancelCharacterAllAbilities()は後で説明します。

死の終わりの関数は今の所使う気がないですが、まあいい…


続いてprotected:の下にこれを加えます。

    FGameplayTag DeadTag;

    FGameplayTag EffectRemoveOnDeathTag;


    // Called when the game starts or when spawned

    virtual void BeginPlay() override;

    

    // Attribute changed callbacks

    virtual void HealthAttributeUpdated(const FOnAttributeChangeData& Data);

もうBeginPlay()あるよっって方が多いと思うので、その場合はBeginPlay()についてはスルーしてください。(publicの方にあるかも)

このHealthAttributeUpdatedがHPが変わった時に実行される関数になります。


続いてキャラクター.cppを編集していきます。

まずはBeginPlay()から。

void Aキャラクター名称::BeginPlay()

{

Super::BeginPlay();

if (AbilitySystem)

{

AbilitySystem->GetGameplayAttributeValueChangeDelegate(UAttributeSet名称::GetHealthAttribute()).AddUObject(this, &Aキャラクター名称::HealthAttributeUpdated);  

        

}

    

}

すでにBeginPlay()あるよって方は、if (AbilitySystem){ } の {}内に上記のうちの

AbilitySystem->GetGameplayAttributeValueChangeDelegate(UAttributeSet名称::GetHealthAttribute()).AddUObject(this, &Aキャラクター名称::HealthAttributeUpdated);  

を加えてください。

ここではAbilitySystem->GetGameplayAttributeValueChangeDelegate()が、Attributeの値の変化が起こった時に、HealthAttributeUpdatedという作った関数を起動するという処理になります。


キャラクター.cppの最後に下記を加えます。

bool Aキャラクター名称::IsAlive() const

{

    return GetHealth() > 0.0f;

}


void AToiroTemplateCharacter::Die_Implementation()

{

}


void Aキャラクター名称::HealthAttributeUpdated(const FOnAttributeChangeData& Data)

{

    OnHealthChange.Broadcast(Data.NewValue);

    

    if (!IsAlive() && !AbilitySystem->HasMatchingGameplayTag(DeadTag))

    {

        if (GetLocalRole() == ROLE_Authority){

            //死のイベントは、サーバーでだけ実施

            Die();

        }

    }

    

}


void Aキャラクター名称::CancelCharacterAllAbilities()

{

    if (GetLocalRole() != ROLE_Authority || !AbilitySystem)

    {

        return;

    }


    AbilitySystem->CancelAllAbilities();

}

HealthAttributeUpdated() が、HPが変更時に呼び出される関数で、if内はHPが0 つまり IsAliveがFalseのときで、なおかつDeadTagがついていないときです。このときDie()関数を呼び出します。

Die()についてはこれから実装していきます。

まずはコンパイルして、エラーが出てないことを確かめましょう。


第二段階:Dieイベントの実装

死んだ時に何を処理すればいいか?ですね。


下準備で、GameModeを継承したBPを作ります。

※GameModeはすでに作っている方だったら不要です。


さて、自分の操作キャラのBPに移動しましょう。

関数のオーバーライド をクリックしてDieを呼び出しましょう

このDieイベントからあとに死の実装をしていきます。まずはCancelCharacterAllAbilities()を呼び出します。これによってこのキャラが持っているすべてのGAをキャンセルします。



とりあえずラグドール化してみました。ラグドール化のやり方はぐぐるとポンポン出てくるので見てください。


サーバーとクライアント側で二回リスポーンしないように、Is Locally Controlledをつないでリスポーン処理を行います。リスポーン処理はサーバーで実施するので、RPC Run On Server「Do Repawn」を作りました。


でもこれ、RPC Run On Serverじゃなくて、はじめからSwitch Has Authority でサーバー側でやればいいんじゃない?と思うかもしれませんが、それだとClientのControllerを渡せないんですよね。これがマルチプレイヤーの面倒なところです。

作ったRPCの中身はこんな感じ


見て分かる通り、実際の処理はGameModeで実施します。理由は、GameModeはサーバーにしか存在しないので間違いが発生しないからです。
ただ、スコアなどのことを考えるとGameStateでもいいのかなという気がしています。これはいつでも修正ができるので、まあ現時点ではGameModeにしておいて、実装が進んだ段階で決めていこうかなと思います。

Get GameModeをCastして、GameMode内で作ったイベント「Respawn Player by GameMode」を起動します。このイベントには、使っているPlayer Controllerの情報と、Destroyするアクターの情報を渡してあげます。

このGameMode内のイベントは上の画像ではRun On Serverになってますが、普通のカスタムイベントでいいです。ちなみに、GameModeはAuthorityでしかアクセスできないので、GameModeにキャストする前にRPC Run On Serverを介する必要があります。

GameMode内で作ったイベント「Respawn Player by GameMode」

はい、中身はこんな感じです。Player Controllerが有効かを確かめて、有効なら新しいアクターをSpawnして、一回コントローラーをUn Possessしてその後、新しく作ったキャラにPossessします。ちなみにですがPossessもサーバー側でしか実施されないノードです。
最後に古いアクターをDestroyします。実際の実装ではSet Timer EventでもうちょいあとにSpawnされるように改良してます。

これでどうでしょうか?実行してみます。

確かに無事リスポーンされるようになったのですが、クライアント側だけリスポーンするとなぜかGAが使えない状態になってます。とんだ縛りプレイだよ。


クライアント側だけがうまく動かないときは、Replicateの設定から見直す

これがマルチプレイヤーゲーム制作の原則です。誰がそう言ってたかって?

俺だよ!!


問題の発生の原因はGASのASC (Ability System Component)のReplicateでしょうね。GAS Documentationを見てみると、それっぽい手順が書いてあったので、早速実践します。(参考文献参照)

    virtual void OnRep_PlayerState() override;

これをキャラクター.hファイルに加えます。protected:の下ですね。


次にキャラクター.cppファイルに下記を加えます。

void Aキャラクター名称::OnRep_PlayerState()

{

    Super::OnRep_PlayerState();

    AbilitySystem->InitAbilityActorInfo(this, this);


}

これでコンパイルして、実行してみると、ちゃんとリスポーン後もASCが起動するようになりました。めでたしめでたし。(この結論にたどり着くまで6時間かかりました笑)

ちなみにですが、もしかすると「俺ASCは、PawnじゃなくてPlayerStateに紐づけているんだけど」という場合があるかもしれませんね。(Fortniteもそうらしいです。) これはRespawnの前後で有しているAbilityなどを保持したいかどうか、などによるみたいです。この場合は、上記のReplicationの記述をPlayerState側に記述する必要があります。

ちなみにちなみにですが、実際の実装では結局GameModeではなく、GameStateのBPを使ってキャラクターをスポーンさせることにしました。GameStateはAuthority以外でもいじれてしまうので、GameStateを使う場合にはSwitch Has Authorityノードを使ってあげると良きかなと思います。

(まあRPC Run On Serverを使っているので無問題とは思いますが…)


おわり