[UE5] GAS でオンラインゲーム Part8 俺を殺したやつは誰だ…を実装する / 自分を倒したキャラを映し出す

Post 2022年5月2日月曜日

GameplayAbility GASでオンラインシリーズ UE5 Unreal Engine

Part 1の一部になります。

前回の続き

ついに今回からUE5のみで検証していきます。今回はGameplay Abilityモリモリでいきます。

お約束
この記事作成にあたって使用した主なUnreal Engine バージョンUE 5.0.1

本日のゴール

これを作る。


出発点

HealthというAttributeが変更された際に、デリゲートを用いて、自動的にDieという関数が実行されるようになっていました。

イベントドリブンな設計で非常に良かったのですが、問題点がありました。

そう、誰から殺されたのか分からないのです。

HealthAttributeUpdatedという関数がその中核ですが、単にHealthが0になったら、Die関数を実行するというだけで、誰に殺されたのかの情報は一切入ってません。

しかし、誰から殺されたのかのを今の設計で分かるのは非常に難しいのです。


なぜ今の設計では難しい?

今の設計ではこんな感じになっています。


キャラ1とキャラ2の間の雪玉のブループリント(BP)がGameplayEffectを介してダメージを与えています。

このBPは、「キャラ1から放たれた」という情報を持っていません。

じゃあ、その情報をもたせればいいじゃないかと思いますが、そのとおりです。


次の問題は、その情報をキャラ2が受け取らなければなりません。

しかし、現状Die関数はなんもアクターの情報とかは持っていません。なのでこれを修正する必要があります。


設計変更

雪玉はGA内でSpawnActorによって、作成することにします。その際に「キャラ1から放たれた」という情報を渡してやります。

次にキャラ2がその情報を受け取るため、Attribute側で死んだかどうかの判断をすることにします。

私のゲームではHPは1しかないので、HPが減る情報は必要ないです。このため、HealthAttributeUpdatedの機能は殺します。(まあ殺さなくてもいいんですけどね)


「キャラ1から放たれた」という情報を雪玉にわたす

Gameplay Effectは、"GameplayEffectSpecHandle(GESH)"の中にある"GameplayEffectContextHandle"の中に、"Instigator(扇動者)"というアクターの情報を格納しています。

※ちなみに"Effect Causer"という情報も格納しておりますが使い分けはよく分かってない。


これがGameplayEffectの作成主ということで運用すれば問題なさそう。


ということは、"GameplayEffectSpecHandle(GESH)をGA内で作成してやり、これを雪玉側で受け取ってGameplayEffectを作ってやれば、あたかもGAを実行した人がGameplayEffectを作成したことにできそうです。



いや何いってんだお前、意味わかんねーよと言われそうですが…

実際に流れを追っていけば簡単です。(多分)


<雪玉のBP>

雪玉側のBPでApplyGameEffectを"ApplyGameEffectSpecToSelf"に変更します。


Spec Handleのところからノードを引っ張って変数へ昇格させます。これが先程の"GameplayEffectSpecHandle(GESH)"です。

この変数は、「インスタンス化」と「スポーン時に公開」をONにしておきましょう。

これで"GameplayEffectSpecHandle(GESH)"を受け取る準備は完了したので、今度は送る準備です。

雪玉を作るGA側に移動します。


<雪玉を投げるGAのBP>

雪玉を作成するSpawnActorに、"GameplayEffectSpecHandle(GESH)"を受け取るInputが作成されていますので、そこに「MakeOutgoingGameplayEffectSpec」を出してつなぎます。そしてGEを指定してやればOK。


Instigatorの情報は勝手に保存されるので、ここで"GameplayEffectSpecHandle(GESH)"に追加する必要はないです。素晴らしいですね。


<2022.5.26追記>

もっとかんたんにできる方法があったので追記

ApplyGameplayEffectToSelfを使った場合


攻撃する側のアクターから、「Get Ability System Component」をつないで、「Make Effect Content」をつないで、「Apply Gameplay Effect To Self」のEffect Contextにつなぎます。

※色々と紛らわしいノードがたくさんあるので、間違えないでください。

攻撃される側のAbility System Componentをターゲットに繋げば、これでGEを与えることができます。

これなら攻撃する側のアクターさえ変数化して、受け渡してやればいいです。


はい。これで雪玉側に作成者(キャラ1)の情報が保存されました。あとは受け取る準備です。こちらがC++をいじる必要があって大変。。。


Attribute側で、"GameplayEffectSpecHandle(GESH)"に格納された情報を受け取る & HP0を感知したらDieイベントを実行

<Attribute Setのcppファイル>

〜Attribute Set Base::PostGameplayEffectExecuteの関数の中で、

    FGameplayEffectContextHandle Context = Data.EffectSpec.GetContext();

という行があって、これが前述の"GameplayEffectContextHandle"になります(Contextの方です、注意)。なのでこのContextから、Instigatorを引っ張ってくれば( GetInstigator() )攻撃してきたアクターの情報が取り出せます。


参照:https://docs.unrealengine.com/5.0/en-US/API/Plugins/GameplayAbilities/FGameplayEffectContextHandle/


ここにある通り、GetInstigator()でAActor*型の情報が取り出せることが分かる。


なので下記の行を上の行のすぐ下に追加します。

  1.     AActor* SourceActor = Context.GetInstigator();

このSourceActorが、攻撃してきたアクターになります。


このSourceActorをDie関数で引っ張ってきたいですよね。

なのでDie関数を変更してやります。

<キャラクター.hファイル>

Die関数の定義があったところに下記を追加します。

  1.     UFUNCTION( NetMulticast, Reliable)
  2.     void MulticastDie(AActor* SourceActor);
  3.     virtual void MulticastDie_Implementation(AActor* SourceActor);
  4.     
  5.     UFUNCTION(BlueprintNativeEvent, Category= "GameplayAbility")
  6.     void Die(AActor* SourceActor);
  7.     virtual void Die_Implementation(AActor* SourceActor);

一番上のMulticastDieは後述するんで、今はコピペだけして読み飛ばしましょう。

<キャラクター.cppファイル>

  1. void Aご自分のCharacter::MulticastDie_Implementation(AActor* SourceActor)
  2. {
  3.     Die(SourceActor);
  4. }
  5. void Aご自分のCharacter::Die_Implementation(AActor* SourceActor)
  6. {
  7. }

<.hファイル>05〜07行が改変部分ですが、Die関数のインプットにSourceActorが入ってますね。これでDie関数をブループリントで引っ張ってきたときに、Die関数からアクターを引っ張ってこれるようになります。

とはいえ今はDie関数に何も情報を入れてないのですっからかんです。Attributeに戻りましょう。


<Attribute Setのcpp>

PostGameplayEffectExecuteの中の、TargetCharacterというのが、Attributeを変更されたキャラ=攻撃された側のアクターとなるので、このTargetCharacterのDie関数を引っ張ってやればいいわけです。

なのでPostGameplayEffectExecuteの中で、TargetCharacterがAlive(=生きているか)を確認して生きていなければDie関数を実行、その際にDie関数に殺したアクターの情報も入れてあげればいいでしょう。

PostGameplayEffectExecuteの中の最後に下記を追加

  1.     if(!TargetCharacter->IsAlive())
  2.     {
  3.         TargetCharacter->Die(SourceActor);
  4.     }

これで終わりです。意外に簡単でしたね。


しかしです。今回はマルチプレイなのでこれでは終わらないです。

Attributeを書き換えたのは、攻撃されたプレイヤーだけなので、他のプレイヤーから見ると攻撃されたプレイヤーのキャラはピンピンしています。

これではだめですね。死のイベントをマルチキャストしないといけません。


このためさっきDie関数を作ったときに、Multicast Dieを作ったわけです。なので、PostGameplayEffectExecuteの中でDieではなく、MulticastDieを呼べば終わりです。

※ちなみにブループリントネイティブイベントは、Multicastへの変更はできません。なのでこのような面倒な手順になっています。

上記に書いた内容を下記に変更

  1.     if(!TargetCharacter->IsAlive())
  2.     {
  3.         TargetCharacter->MulticastDie(SourceActor);
  4.     }


Die関数が変更されたことを確かめる

Die関数にActorのピンが出ていることが分かります。あとは攻撃してみて、Die関数が動けば終わりです。



カメラの切り替え

カメラの切り替えは色々解説しているサイトが別にあるので、あまり深くやるつもりはないです。


SpawnActorでカメラ(Camera Actor)を作成し、これをAttachActorToActorでSourceActorにアタッチしてあげます。※Spawnしたカメラはかならず変数に昇格しておきます。(あとで消去するため)

SpawnActorでスポーンする位置(座標)については、SourceActorからGetActorLocationを引っ張ることで、計算を行います。

AttachActorToActorでWorldにするかRelativeにするかでも計算は変わります。私はWorldにしましたが。

最終的にSetViewTargetWithBlendで、カメラを移動します。全部終わったら、カメラを削除して終わりです。


以上です!