そうだ、ゲームを作ろう

現状や学んだことなど記録するブログ。間違ってたらごめんね

UE4ぷちコン映像編3rd「Living in the Darkness」振り返り

お疲れさまです。
今回ヒストリア様主催、第3回ぷちコン映像編に応募し、最優秀賞を頂くことができました。ありがとうございました。

さて生放送で制作過程を知りたいとの方が多くいらっしゃったので、振り返りも兼ねてここに書いていきたいと思います。

件の作品は↓

まずはお品書き

・ストップモーション的なアニメを作ろうと思ったきっかけ

・マテリアルはどうするか?

・モデルはどうするか?

・ストップモーションらしく見えるために

・ライティング

・MovieRenderQueue

・苦労したところなど

・反省点とか

・最後に

 

 

ストップモーション的なアニメを作ろうと思ったきっかけ

次のぷちコン映像編までに一応のやりたいことみたいの考えとかないとな

そういえばLAIKAは結構映像作品にCGを使っていると聞いたことがある

Blenderクレイアニメ作ってる人もいるし、UEも当然あるんだろうなぁ…

一応探してみて…え?もしかして無いの?

 

…ということで「意外と今までUEでストップモーションアニメに挑戦した人がいない」という事実が判明したので(まあ僕の知らない所でいるのかもしれませんが)、じゃあ自分が挑戦してみるかぁ、と少し思っていました。

テーマや実際できるかどうかとの兼ね合いもあるので、せいぜい「ストップモーションっぽいのができたらいいなあ」という程度のものでしたが…

 

・マテリアルはどうするか?

とりあえずテーマ発表前後に「果たして本当にUE内でそれっぽいことはできるのか」検証することにしました。最初に検証したのは当然マテリアルです。PBRテクスチャの指紋フリー素材を見付けたので、これでどうにかならないかと思いましていろいろ試したら便利なノードが見つかりました。NormalFromHeightMapというノードです。これを色を反転させた先の素材と組み合わせます。

f:id:wvigler:20220122073529p:plain指紋の表現はこれでできると分かりましたが、実際には粘土っぽいSurfaceを持ったMegascans(soilとかdirtとかの名前付いてるのが多い)と組み合わせています。今回は面倒だったのでMegascansのPresetに直接書き込んでいます。

f:id:wvigler:20220122074125p:plain良い子のみんなはあんまりこういうことやっちゃダメだぞ!
ついでに水や煮炊きの表現に使用されていた波立っている表現ですが、これもPresetで使用されているMaterialFunctionであるMF_MapAdjustmentsに直接書き込んでいます。良い子(ry

f:id:wvigler:20220122075050p:plainこれをWorldPositionOffsetに接続しています。(右下ただ2で割るだけでいいと思うんだけどどうしてこうなってるのか…もう実装したの大分前のことで記憶が…)

WPONoiseIntensityをある程度の数字にして、WPONoiseCoordinateを毎フレームRandomFloatで動かすことによって、波立つ表現を可能にしています。

f:id:wvigler:20220122080705p:plainこれをこうして

f:id:wvigler:20220122081023p:plainこうじゃ!

f:id:wvigler:20220122081120p:plainTimeは秒間12Fなので1/12ですね。WPONoiseIntensityを操作することで波立ちを抑えたりもできます。

水道に関してはSplineMeshを使用しています。

f:id:wvigler:20220122082615p:plainこれも上と同じマテリアルを用いていますが、RandomFloatを使用するのではなく、下にテクスチャが流れるような表現をしています。

f:id:wvigler:20220122082324p:plain今見直してみると、指紋まで移動させる必要は無かったような…

それからライト関係はこれとは違うごく普通のマテリアルを用いています。現実のストップモーションでもライトは普通の電球だったりするので雰囲気作りで。

f:id:wvigler:20220122083123p:plain↑作ったのに全く画面に映らなかった玄関のライトさん。折角なのでお披露目。

 

・モデルはどうするか

まぁ、なんというか、全部、自作、ですよねぇ…(笑)

前回同様、今回もモデルは全部自作です。お借りしたのは音素材と天球(一応外に空があります)のみです。Blenderで制作してます。

壁、床、天井、鍋、水、それから主人公はメッシュ自体に凹凸はついておらずつるつるなんですが、他のクレイっぽいものは一応メッシュにも凹凸を付けてます。まあNormalによる凹凸感が思った以上に効くので(壁が実は平面だって気付かなかったでしょ?)、どこまで有効だったのか分かりませんが、とりあえず作り方などを…

f:id:wvigler:20220122084032p:plainデフォルトキューブをクレイ化させます。サブディビジョンをかけたいのですが、角の部分はある程度残って欲しいので、まずはベベルをかけます。

f:id:wvigler:20220122084329p:plainなんとなく三角ポリゴンがあるのが気に入らないので分割数は2で。残したい部分がある場合は、適宜この段階でエッジを追加します。サブディビジョンした後でも戻せるのでそこまで慎重になる必要はないです。

f:id:wvigler:20220122084731p:plainサブディビジョンをかけた後です。

f:id:wvigler:20220122085411p:plain見た目の確認をしながら調整したいので、ShadeをSmoothにします。(これは別にどの段階でかけても構いません)

すでにちょっとクレイっぽくなっていますが、さらにDisplaceを使用して凹凸を付けていきます。

使用するテクスチャを調整します。僕はたいていCloudsを使用します。

f:id:wvigler:20220122090030p:plain

f:id:wvigler:20220122090337p:plain

SendToUnrealとの関係でUnitScaleをデフォルトから弄っているので、このパラメーターは信用しない方がいいです。要は見た目重視で色々調整してください。

見た目はこんな風になりましたf:id:wvigler:20220122090447p:plainこれでかなりクレイ感が出たので、UVの調整などをしてUE側に持っていきます。

 

ストップモーションアニメらしく見えるために

マテリアルやメッシュ以外でストップモーションアニメらしく見せるためにいくつかやったことがあります。

まず当然ですがFPSは落とします。だいたい12フレームあたりからストップモーションに見え始めて、6フレームあたりまでが限界な気がします。(これ以上やるとただのコマ飛び動画みたいに見えてしまう)実際のストップモーションアニメのFPSはまちまちらしく、おそらくLAIKAなどは24フレーム使用しています。

次にモーションブラーを切ります。これはPostProcessのRenderingFeatures>MotionBlur>Amountを0にすることで実現できます。f:id:wvigler:20220122092512p:plain

実際のストップモーションアニメでモーションブラーを付ける方法は無いわけではないんですが、まあ今回は論理より雰囲気重視ということで。

アニメーションで気を付けるべきことはなるべくポリゴン貫通は避けるということです。鍋をつかむ際など意図的なものは見逃されると思いますが、目立つ場所でバリバリポリゴン貫通させてしまうと折角マテリアルで付けたリアリティが台無しになってしまいます。疑似ストップモーションでは「そこに実際にあるように見せる」というのが非常に重要だと思います。

アニメーションの「ブレ」みたいなのも再現した方が当然雰囲気としては上がるんですが、やるとなるとめちゃくちゃ面倒臭いので今回はパスしました。

それからAnimationのInterpolationをLinearからStepにした方がいいんじゃないかと思いましたが、実際の映像を撮ってみたら、やらなくても特に変わらないのでやらなくなりました。

掴んだ後の鍋の動きとか、ゴミの動きとかは完全な手付けですね。こういうのを手付けでなく処理できるような上手い方法が発見されるといいんですが…アタッチするのも却って問題が起きるし…

 

・ライティング

GIはリアリティがぐっと増すので、こういった作品にはもってこいですね。今回GIとしてLumenを使用しています。正直使ってみたいが先行して使ってみました。レイトレと比較して長所も短所もあり、どっちが上というわけではないですが、結構苦労したのが…f:id:wvigler:20220122191057p:plain

f:id:wvigler:20220122191259p:plainこの光漏れノイズです。最初の方は壁や床を板ポリ一枚にしていたため、暗くなるとイルミネーションの如くとなり相当深刻でした。あとから壁を厚くすればかなり防げると気付きましたが、細かいところは残ってしまいますね。家具を壁にピッタリくっつければ更に少なくなるみたいなんですが、「そんな家具の置き方する人いないよなあ」とリアリティ重視の名のもとに切り捨ててしまいました。

それからもう一つ、Lumenは映像が動くと残像のようなノイズがその後に残ります。普通の映像作品ならそこまで気になるものではないんですが、ストップモーションで前のコマが残るのはかなり困りものです。

これについてはMovieRenderQueueを使用することである程度防ぐことができました。

 

・MovieRenderQueue

今回リアリティのある絵作りができているのはMovieRenderQueueの威力がかなり大きいです。一度これをやってみると、SequencerのMovieCaptureはあくまでプレビュー用だな、という認識になってしまいました。MovieRenderQueueについては↓

連番画像でしか出力できないのが弱点ですが、Captureに比較してかなり高品質な映像になります。僕の設定はpng出力でアンチエイリアスは以下のようになっています。

f:id:wvigler:20220122125334p:plain

Temporal Sample Countはあんまり上げすぎるとMotionBlurのような残像が出てしまうので少なくしています。あくまでほんの少しなので気にする必要ない気もするんですが…

 

・苦労したところなど

●キャラクターらしさを出そうとして主人公モデルを作成したが、全体的に横幅が広く、色々な所にぶつかりやすい。ぶつかるとポリゴン貫通が発生するのでまずいことになる。特に手首から下が前腕より長く、大きいため色んな所にぶつかる。ぶっちゃけアニメーション作りにくい。これが一番苦労したかも。

●コンロの火。確か「Kubo」に紙で火を表現した場面があったよなと思い、その記憶から同じことをしようとしたが、リアリティのある紙の動きが難しすぎて断念。(そもそもKuboの炎はコンロではないためそこにも無理があった)最終的に今の形に落ち着いたが、一番の悲劇はこんなに苦労した上にちゃんとうまく行ったのに画角の関係上、別にそんなに大きく映らないこと。f:id:wvigler:20220122192108p:plain一番大きく映ってこれ(涙)。ぜひよく見てやってください。

●投げるゴミ。BlenderでClothSimulationして作成したが、なんかそれっぽくならない。ゴミを潰す所から撮影しようとも思ったが、ClothSimulationした結果を連続でキャプチャーしてStaticMeshをフレームごとに入れ替える?正気か?という理由により妥協した。激しく動くものは手付けもなかなかうまく行かない。どこの地点で各コマをキャプチャーしてるのかいまいち分からないため、腕に埋まってしまったりとか。

●音。毎回苦労させられる。今回は音合わせとかしないからまだマシかも。でも虫のカサカサ音とかなかなか無い。

●気付かれましたか?f:id:wvigler:20220122190606p:plain「UE5」とベッドに描かれている。

 

・反省点

●別にそんな要素は無かったのに、余裕こいたせいで最終日かなりわちゃわちゃしていた。締切が近付かないと頑張れないタイプっているよね。

レンダリングミス2箇所。f:id:wvigler:20220122190343p:plainf:id:wvigler:20220122190852p:plain

●結局Sequencerがエディターを動かしている時に、どういう挙動をするのかいまいち分かってないことがちらほら。

●それなりに生活感のある(つまり汚い)部屋にしようとしたけれども、もっと色々置かないと生活感が出ない。プロップをもっと素早く作れるといいんだけど…。

●本物のクレイアニメみたいに物を自由に変形させたり、一旦キャラクターを別の形にしたりはできることはできるけど、手間がものすごく掛かってしまう(それこそ本物を撮った方が楽なほどに)。何か良い方法が見付かればいいんですが…

 

・最後に

映像編2連覇ということで、ご評価いただき本当にありがとうございます!今回は偶然年始あたりに大きく時間が作れたので、いい作品を作れたと思っております。もちろん全てが思い通りに行ったわけではないですが…

ともあれ、凄い作品が多い中で自分の作品が最優秀賞を取れたことはとても嬉しいです。

主催のヒストリア様、ならびに参加者、関係者の皆様に改めて御礼申し上げます。

みんなありがとう!これからもよろしくね!

第16回UE4ぷちコン「太極雙陸」振り返り?~ネットワークについて~

さて、時間経ちすぎてるだろというつっこみは置いておいて、前の記事で書きましたが、第16回ぷちコンで制作して、まだver0.9.0だった「太極雙陸」のデータが消失しました(まだ少しだけ残ってるけど)。

まぁ自分の開発体制なども振り返ってみると起こるべくして起こったというか、起こって当然というか、なんならこのまま続けたらまた同じことが起きるぞ、という感じです。

反省としてソースコントロールを学んだのと、GitHubについても学ぶので勘弁してつかあさい!

 

さて!ここ一ヶ月というもの「太極雙陸」のオンライン化を進めてきました。一応、完成まではできました!(モノはありませんが…)

ということでここ最近ネットワークについて手に入れた知識をざっとではありますがまとめていきたいと思います。

 

お品書き

・Server, Client, Dedicated, Listen

・ネットワークをテストする

・ActorReplicate

・ReplicateMovement

・VariableReplication

・RPC(RemoteProceduralCall)

・Reliable

・ネットワークにおける各クラスの立ち位置

・Client, Serverで処理を分けたい場合

・Lobbyについて

・SeamlessTravel

・ありがちな落とし穴

・どうやったらコツを掴めるか

・サンプルプロジェクト

 

・Server, Client, Dedicated, Listen

UEではネットワークにはServer-Clientというモデルを採用しています。これは中央にあるServerの役割を担うマシンがゲームを統括し、ClientはServerから渡される情報を元にして各マシン上でゲームを構成する、というモデルです。形態には二種類あり、ServerがServerとしての役割のみを果たすDedicatedServerModeと、Clientのマシンが同時にServerとしての役割を果たすListenServerModeがあります。

DedicatedServerの利点は負荷の低さと、それに伴う通信品質の向上、そしてClient側は皆平等な品質が保証されることです。さらに構造自体はListenよりも単純なため、実装の手間も若干低いです。(ただしDedicatedServerを使用するプロジェクト自体がある程度大きい場合が多いです)

逆にListenServerの最大の強みはレンタルサーバーなどサーバーコストが一切掛からないということです。そのためインディーズやフリーゲームなどはどうしてもこちらになりがちですが、Serverになったマシンの負荷が高く、ちゃんと設計しないとラグくてどうしようもなくなったりします。またClient側のマシンが通信を切断してもゲームが継続できるのに対し、Server側が通信を切断するとゲームが続行不可能だったりと、様々な面でServer側とClient側の差異が出てきてしまいます。

 

・ネットワークをテストする

f:id:wvigler:20211023020158p:plain

Playのオプションで手軽にPIEでネットモードを試せます。

NumberOfPlayersはPlayerの人数です。これが2であることを前提にすると…

PlayStandalone->デフォルト設定です。2つの全く違うマシンとして実行します。マシン同士の接続からテストする際によく使用します。

PlayAsListenServer->2つのマシンの片方がListenServer、片方がClientとして実行されます。とりあえず手軽にListenServerの状態をテストしたい時に使用します。

PlayAsClient->双方がClientとして実行されます。DedicatedServerのテストなどの際に使用します。

いくつかのサンプルプロジェクトはネットワークに最初から対応しています。例えばThirdPersonTemplateなどは作ってからすぐにネットワーク対応ができていますので、PlayAsListenServerやPlayAsClientで体験してみるのが良いかと思います。

 

・ActorReplicate

UEのネットワークの最小単位はActorです。Actorには「Replicates」というBooleanが付いています。

f:id:wvigler:20211023015455p:plain

これをチェックすることによってServer-Client間でこのActorが共有されます。このままだとReplicateされるのはSpawnおよびDestroyされた時の処理のみです。実際このActorをレベルに配置すると、Client側だとDestroyが通らず、Server側でDestroyした場合にはServer側のActorと同時にClient側のActorも消えます。(当然何かしらで可視化してないと観察できませんw)

これに限らず共有は基本的に上位であるServerから下位であるClientに行われ、Client側から直接共有されているActorにアクセスしようとしても拒否されて何も起きません。これをするためにはClient側からServerに「このActorにこういう動作をさせてください」という「お伺い」を立てる必要があります。

さて、このままだとSpawn-Destroy以外の動作や数値などなにも共有されないので、これらを共有するためには他にも色々する必要があります。ただし、以下のものはまずActorReplicateが動作している(つまりこれがチェックされている)ことが前提です。

ActorReplicateをチェックせずに以下をやっても何も起きないので注意しましょう。

・ReplicateMovement

同じくActorに付いているBooleanとしてReplicateMovementというパラメーターが存在します。

f:id:wvigler:20211023021512p:plain

これはActorの位置情報などをReplicateするものです。かなり便利な機能ですが、通信する度にいちいち情報の共有が行われるため帯域の圧迫が比較的大きいです。「太極雙陸」ではネットワークで石の動きをこれで共有しようとしたため、かなりカックカクになってしまいました。なので動作の共有に関してはよく考える必要があると思います。

・VariableReplication

Variable変数にも共有機能があります。Replicationというパラメーターです。

f:id:wvigler:20211023053419p:plain

3つのEnumが使用されており、それぞれ

None->共有しない。

Replicated->共有する。

RepNotify->共有する。更に共有した際に特定の関数を(Server, Client双方で)実行する。

となっています。RepNotifyを選択すると自動的にOn_Repホニャララという関数が作成され、それが共有時に実行されます。

共有するタイミングは基本的に変数が変更された際ですが、RepNotifyはちょっと変わった動作があります。"Set"以外で変数が変更された場合(例としてはArrayを操作するAdd、Remove、Clear、Resize、Reverse、SetArrayElemなど)、On_Rep関数はClient側でしか動作しません。

恐らく基本的にRepNotifyはSetで変更されることを前提に動いているためだと思われます。

・RPC(RemoteProceduralCall)

Replicationが変数の共有ならば、こちらは動作(Event)の共有です。これもEnumでいくつか種類が用意されています。

f:id:wvigler:20211023061453p:plain

NotReplicated->共有しない

Multicast->基本的にServer側で使用。ServerとClient双方で同じEventを実行する。

RunOnServer->基本的にClient側で使用。Server側で実行させる。イメージとしてServerに「お伺い」を立てるために使用するEvent。

RunOnOwningClient->Actorを所有しているClient側で実行させる。UI関連で使用することが多い。

どれもかなり重要な動作になりますが、一番使用頻度が高いのはRunOnServerでしょうか?MulticastとReplicationはほぼ同じ動作ができますが、信頼性としてはMulticastの方が高く遅延しにくいです。(もちろんその分帯域を食います)

・Reliable

更にRPCにはReliableというBooleanパラメーターが用意されています。

f:id:wvigler:20211023062447p:plain

Reliableとそうでない場合にはそもそも通信に関する動作が全く違うのですが、それを説明するととても複雑なので、基本的にReliableがチェックされている場合は遅れたとしてもその動作は確実に実行される。逆にチェックされていないと実行されない場合がある。ただしReliableの方が帯域を食う。という感じに覚えておけばいいかなと思います。もっと詳しく知りたい方は以下のスライドが参考になります。

 

・ネットワークにおける各クラスの立ち位置

シングルプレイでのゲームばかり開発していると、疑問に思うことがあります。「GameModeとPlayerControllerって一緒にしてもいいんじゃない?」「このBPの候補PlayerStateとかGameStateってActorは何?」など。
ネットワークに触ってみると、GameModeとPlayerControllerは実は全く違う動作をするクラスなのだと分かります。
このようにシングルプレイとマルチプレイのゲームではそれぞれのクラスの役割がかなり変わっています。なのでこれらがどのような役割を担っているのかを見直していきます。
 
・GameMode
GameModeの最大の特徴は全マシン上でもServerに一つしか存在しないという点です。なのでClient側でGetGameModeを呼び出してもnullptrが返ってきます。GameModeはその唯一性からターン情報や勝利条件、ゲームのセッティングなど、全てのClientが共通で持つべき情報を所有するのに適しています。ちなみにGameModeBaseクラスはGameModeクラスの機能縮小版です。
特に重要なイベントノードとしてOnPostLogin、OnSwapPlayerControllersが挙げられます。

f:id:wvigler:20211023070635p:plain

ともにレベル遷移の際に使用するノードなのですが、OnPostLoginは通常のレベル遷移、OnSwapPlayerControllersはSeamlessTravel(後述)の際に使用します。どちらもアクセスしてきたPlayerControllerが得られるのが特徴で、ClientがGameModeから各PlayerControllerにアクセスできるよう、通常はこれをArrayにして保有するというのが基本になります。

f:id:wvigler:20211023071213p:plain

こんな感じに。
 
・PlayerController
PlayerControllerはそれぞれのPlayerが入力などを行うために所有するクラスです。ServerとClientの橋渡しをする役目を担っており、マルチプレイゲームを作成するとかなりごちゃごちゃします。RunOnServerイベントは基本的にこのクラスから発信されることが多いです。
PlayerControllerはお互いのPlayerControllerを知ることができないため、他のPlayerControllerに何かしてもらいたい場合には一旦RunOnServerでGameModeをGetし、そこからPlayerControllerにアクセスしてもらうというようなやり方をします。
 
・GameState、PlayerState
今回はあまり使用していないです。こちらの記事がとても分かりやすいので割愛。
 
・UMGなどのWidgetなど共有されていないもの
UMGを始めとするWidget、UIの類は基本的にPlayer同士で共有しません(するゲームもあるとは思いますが)。つまり共有されていないものにアクセスすることになるので、これらにServer側からアクセスするには基本的にRunOnOwningClientを使用します。
 
・その他の共有されたActorなど
これらはServer側が所有権を持っているため、Clientからいくらアクセスしても何もしません。Server側から変更した変数のReplicationや、RepNotify、もしくはMulticastで動作させることができます。
 

・Client, Serverで処理を分けたい場合

ClientとServerで処理を分けたい場合にはActorの場合SwitchHasAuthority、UMGの場合はIsServerを使用します。

f:id:wvigler:20211023212757p:plain

f:id:wvigler:20211023212931p:plain

・Lobbyについて

PIEでのテストの場合、いきなり最初から何人かのプレイヤーが存在しているような状態になっていますが、実際のゲーム上ではそんな事はありえません。プレイヤーが”集合”する場所を作成する必要があります。
大体のゲームではこれにLobbyというLevelを使用しています。Lobbyの作成及び接続にはCreateSession、FindSessions、JoinSessionなどのノードが使用されます。

f:id:wvigler:20211024014315p:plain

Lobbyへの接続形態としては、基本的にServer側はCreateSessionを実行してOnSuccessでLobbyLevelにOpenLevelしてそこに待機し、Client側はFindSessionでみつけたSessionArrayから参加できるSessionを探してJoinSessionノードを実行する、という流れになります。(Join後にLevel遷移を処理する必要はありません。自動的に遷移します)
またSessionから抜けるためにはDestroySessionを使用します。

f:id:wvigler:20211024014458p:plain

Server側が落ちた際にSessionから抜け出せていないと、新たにSessionに接続できなくなるので、例えばEventEndPlayなどにDestroySessionを繋げばそのような動作を防ぐことができます。

f:id:wvigler:20211024014711p:plain

・SeamlessTravel

さて、LobbyLevelは単なる集合場所に過ぎないので、そこからさらにゲームを実際に行うLevelまで移動しなければなりません。その時に使用するのがSeamlessTravel(ServerTravel)です。

原理的な説明はこのスライドが詳しいです。実際にSeamlessTravelはBPには公開されていないためConsoleCommandを叩く必要性があります。使い方は以下のように…

f:id:wvigler:20211024020121p:plain

Serverがこれを実行すればClientごとSeamlessTravelを行います。
その後SeamlessTravelした先のGameModeのOnSwapPlayerControllersに処理が流れます。

f:id:wvigler:20211024022816p:plain

これが実質的なSeamlessTravelにおけるOnPostLoginとなります。
SeamlessTravelはPIEに対応していない(対応してくれ…マジで…)ため、これ以降をテストする際にはStandaloneで実行する必要があります。
 

・ありがちな落とし穴

▼なぜかServer側でしか実行されない。

f:id:wvigler:20211024021356p:plain

GameModeなどの参照をBeginPlayした時に保存して持っておく、というのは結構やったりしますが、Client側だとGetGameModeはnullptrが返ってきます。上のようにすると、当然Castには失敗し、CastFailedに処理が流れているためDoSomethingには処理が流れません。
Initialize処理などがServerでしか走らない場合、こうなっていたりします。エラーが出ないのが本当に恐ろしい…
 
▼OnSwapPlayerControllers後のGetPlayerController

f:id:wvigler:20211024023708p:plain

OnSwapPlayerControllersが走っている間はまだ前のPlayerControllerが登録されているため、それを考慮しないとGetPlayerControllerなどが妙な動作をしてしまいます。処理が終わればきちんと無くなりますので、Delayを使用するのも手です。
 
▼MaterialInstanceDynamicはReplicateしてはいけない
ContentSampleの信号機のサンプルでもMIDはReplicationがされていません。これを試しにReplicationすると分かるのですが、エラーが出ます。
どうやらMIDはReplicateしてはいけないらしいです。余談ですが「太極雙陸」ではまだネットワークに対しての理解が浅かったので、爻(こう)の各個情報としてMIDをStructureに含めてしまったため、他の爻の情報をReplicateすればMIDがReplicateされ、MIDをReplicateしなければ他の情報がReplicateできないということになり、結局MID情報をStructureから切り離すハメになりました。

f:id:wvigler:20211024064034p:plain

↑消えたプロジェクトの残滓から発掘したStructure情報。上からStaticMeshComponent、現在の色、置いてある石、MID、石を整列させる時に使用するSpline。なんとなくやったこんなことが後に悲劇を生むとは…
 

・どうやったらコツを掴めるか

手を動かす。
才能のある人ならともかくとして、僕の場合はどうしても頭で理解するのには限界があるので、実践で感覚的なものを掴んでいくしかない感じでした。
実際、一つ作ってみると覚束ないながらもだんだんと分かっていくので、おすすめです。なんというか、プログラミング分野にもなんだかんだ「感覚で覚える」ってのはあるんですよねぇ…
 

・サンプルプロジェクト

最後にお土産です。Githubにチャット機能のサンプルプログラムを公開いたしました!簡単なものですが、皆様のご参考になれば幸いです!チャット機能は比較的簡単に実装できますので、ネットワークの初学としてオススメです。(SeamlessTravel(デバッグがとても面倒臭い!)をする必要もないし)
ご指摘、ご質問、ご意見などあればコメントにお願いします。

「太極雙陸ver1.0.0」のデータがほぼ全消失しました…

個人制作物だし、学習用コンテストだし、まあ最悪飛んでも良いかと思っていたのは事実だが…

 

先程「太極雙陸ver1.0.0」(LAN対応版)のデータをパッケージングした完成版ごと誤って消去してしまい、その後リカバリソフトなどを使用しても復旧できないことが確定しました。

みんな!バックアップはちゃんとしておこうぜ!

いや個人制作でこういう経験ができて、却って良かったと納得するしかないなこれは…orz

ネットワークに関しての知見は沢山得られたので、後で記事にします。

第15回ぷちコン「たぬ吉の大冒険」振り返りその4<GameplayCue編>

この記事はこの記事の続きです。

 この記事では第15回ぷちコンの振り返りとして、「GameplayCue」の解説をしていきます。

 

お品書き

・GameplayCueについて

・GameplayCueEditorとGameplayCueManagerについて(GameplayCueを扱う準備)

・GameplayCueNotify_Actorについて

・GameplayCueNotify_Staticについて

・GameplayCueを実装してみる

・あとがき

 

・GameplayCueについて

GameplayCueはGamplayAbilitySystemにおいて視覚エフェクト(パーティクル等)やサウンドエフェクトなどを司るシステムです。

GameplayCueのHandlerはObjectであるGameplayCueNotify_StaticとActorであるGameplayNotifyCue_Actorの2つに分かれ、それぞれに役割があります。ただ扱い方をちょっと間違えると意図しない動作を引き起こしたりと、結構難しい部分のあるシステムだなと感じました。

・GameplayCueEditorとGameplayCueManagerについて(GameplayCueを扱う準備)

GameplayCueは使用するノードなどを見ると、一見普通のGameplayTagで呼び出せそうに見えますが、実は普通にやっても呼び出せません。GameplayCueを扱うには2つの前段階が必要です。それはGameplayCueEditorを使用してGameplayCue.~のTagを作成することとGameplayCueNotifyPathsにGameplayCueHandlerのフォルダを登録することです(ちなみに厳密にはどちらも必須ではないみたいなのですが、話がややこしくなるので必須という体で進めます。ま、大抵これをやるよってことで)。

まずGameplayCueEditorです。これは「Window」のタブメニューから呼び出すことのできるEditorの一つです。

f:id:wvigler:20210501181128p:plain

これを開くとこのような画面になります。

f:id:wvigler:20210501181629p:plain

AddNewGameplayCueTagを開くと…

f:id:wvigler:20210501181822p:plain

このようにGameplayTagの追加が行えます。そしてGameplayCueで扱うGameplayTagは必ず「GameplayCue.~」で始まる必要があります。

それではとりあえずGameplayCueEditorから新たなGameplayTagを登録します。名前は「GameplayCue.Attack01」とします。

f:id:wvigler:20210501182717p:plain

Handlersという項目がありますが、GameplayCueとTagが関連付けられると、ここに関連付けられたHandlerが表示されます。それだけでなく「AddNew」をクリックすると、予め関連付けられたHandlerを作成することができます。

f:id:wvigler:20210501183306p:plain

説明文も出てとても親切ですね。

さて、先程HandlerとGameplayTagが関連付けられている、と述べましたが、実はこの関連付けはEditor上だけのもので、Game上は全く紐付けされてはいないようです。

で、Handlerを操作するにはいくつかのノードを介するかGameplayEffectのDisplayで行うのですが、

f:id:wvigler:20210502040019p:plain

f:id:wvigler:20210502042446p:plain

いずれもGameplayTag情報のみから呼び出す形を取っています。そしてGameplayAbilityにおけるAbilityListのようにいくつかのGameplayCueHandlerを登録する、といった機能はありません。

じゃあどうやってTag情報だけでそれぞれのHandlerを探しているのかというと、GameplayCueの呼び出しはデフォルトの状態では(GameplayCueManagerというクラスが)プロジェクトファイル内の全てのファイルを走査して同じGameplayTagを持っているHandlerを探しています。

どうやら2回目以降は紐付けがなされているらしく素早く探せるようになるのですが、なんとプロジェクトを一旦閉じ、再起動して実行すると再度この処理が必要になります。プロジェクトファイルが大きくなればなるほど、プロジェクトを起動してGameplayCueを初回発動した時にフリーズしたような停止時間が挟まるというとんでもない状態になります。

これを防ぐためにはGameplayAbilityがAbilityListを使用したように、走査する範囲を限定する必要があります。そのためGameplayCueを1箇所もしくは何箇所かのフォルダに集め、GameplayCueNotifyPathsにそのフォルダを登録します。

f:id:wvigler:20210501190911p:plain

このようなフォルダを作成し、DefaultGame.iniに書き込みます。

f:id:wvigler:20210501193152p:plain

以降、GameplayCueのHandlerは全てこのフォルダに投入します。

まだよく分かっていないのですが、どうやら起動時にこの走査処理を予めまとめて行う解決法もあるようです。このあたりの選択肢を残すためにわざと初期状態だと問題が起こるようにしているのかもしれません。

・GameplayCueNotify_Actorについて

GameplayCueNotify_ActorはGameplayCueのHandlerの一つでその実態は普通のActorです。どのぐらい普通のActorなのかというと、本来これ自体は見えないものとして作られているにも関わらず、StaticMeshComponentなどを追加すると可視化することができたり、Level上にドラッグ&ドロップで普通に配置できたりします(どっちもあまり意味はありません)。基本的には実装されている"OnActive"や"OnRemove"あるいは"WhileActive"といったFunctionをOverrideして使用します。一定の期間パーティクルを出し続けたりなど、ある程度継続してエフェクトを実行し続けたい時に使用します。

・GameplayCueNotify_Staticについて

こちらもGameplayCueのHandlerの一つなのですが、Actorではなく、どちらかというと生成するActorの情報を持っているObjectです。このObjectから生成されたGameplayCueNotify_Actorは一瞬で生まれ、また一瞬でDestroyされます。基本的には"OnExecute"をOverrideして使用します。例えば爆発の視覚エフェクトや効果音などの、瞬間的なCueの実行に向いています。

・GameplayCueを実装してみる

それでは実装してみましょう。GameplayCueEditorからGameplayCueHandlerを作成するのは先程説明したので、あえて普通のBlueprintClass作成から作っていきましょう。GameplayCueNotify_Staticを作成します。

f:id:wvigler:20210502040402p:plain

名前はGQS_Explosionとします。もちろんですが「GameplayCue」フォルダに作成することを忘れないでください。

f:id:wvigler:20210502042023p:plain

GQS_Explosionを開くとこうなります。

f:id:wvigler:20210502045112p:plain

EventGraphは何も書き込まないので意味がありません。ここで注目するのは右のClassDefaultsです。

f:id:wvigler:20210502045305p:plain

GameplayCueTagという項目がありますが、これがトリガーとなるGameplayTagになります。さて、GameplayCueNotify_StaticはOnExecuteをOverrideすることで機能を実装します。早速OnExecuteを開きましょう。

f:id:wvigler:20210502052656p:plain

OnExecuteの中身はこのようになっています。今回は用意が面倒なので視覚エフェクトや効果音はStarterContentから取ってきます。このクラスがExecuteで呼び出されると自身を中心に爆発のエフェクトと効果音が鳴るようにしていきます。

このように繋げます。

f:id:wvigler:20210502065208p:plain

MyTargetとParametersはParentノードに繋げないとエラーになります。これで中身は作ったので、後は紐付けです。先程のGameplayCueTagを"GameplayCue.Attack01"にします。

f:id:wvigler:20210502065751p:plain

そして今度はGPA_Attackに移動し、ExecuteGameplayCueOnOwnerでGQS_Explosionを呼び出します。

f:id:wvigler:20210502065948p:plain

これで準備完了です。実行しましょう。

f:id:wvigler:20210502070413p:plain

Zキーを押すと剣を振るとともに上のように爆発のエフェクトと効果音が鳴ります。ここでは使用していませんがExecuteGameplayCueWithParamsOnOwnerというノードもあります。これはParametersというStructureを通してかなり様々な情報をHandlerに送ることができます。

f:id:wvigler:20210502071704p:plain

ただこのParametersはこのノードを介さなくてもGameplayEffectからの実行時などにある程度の情報を運んでいるようです。ちょっとだけ見てみましょう。

f:id:wvigler:20210502072905p:plain

今は使用していないGPE_ReduceHealthにGQS_Explosionを紐付けます。MagnitudeAttributeはGASAttributeSet.Healthにしましょう。

f:id:wvigler:20210502073128p:plain

再びCキーに繋げます。GQS_Explosion側ではParametersからRawMagnitudeを表示するようにします。

f:id:wvigler:20210502073402p:plain

これで実行してCキーを押すと…

f:id:wvigler:20210502073530p:plain

GQS_Explosionの発動とともに、このようにHealthの変化値である-80.0が表示されます。ただここは僕もまだ良く分かっていないのと、詳しく調べるときりがなさそうなので、誰か調べてください(丸投げ)。

さてCキーのノードは再びWeaponExpandのノードに元のように繋げまして…

f:id:wvigler:20210502073919p:plain

今度はGameplayCueNotify_Actorを使っていきます。これを使用して剣を大きくしている際には常時炎のエフェクトを出すようにしましょう。

まずはGameplayCueNotify_Actorを作成します。

f:id:wvigler:20210502080005p:plain

名前は"GQA_Flame"としました。

f:id:wvigler:20210502080309p:plain

中身を見てみましょう。

f:id:wvigler:20210502083220p:plain

違いとしてActorなのでViewportやConstructionScriptがある点と、右のClassDefaultsがGameplayCueNotify_Staticに比較してかなり充実している点が挙げられます。

ViewportやConstructionScriptは正直使わないので、ClassDefaultsを見ていきましょう。

f:id:wvigler:20210502084004p:plain

注目したいのはCleanupとGameplayCueの2カテゴリです。ここがGameplayCueの動作に関わってきます。GameplayCueTagはStaticの場合と同じですし、多重発動の有無など細かい項目もありますが、とりあえず重要なのはCleanupカテゴリのAutoDestroyOnRemoveとGameplayCueカテゴリのAutoAttachToOwnerです。

AutoDestroyOnRemoveはAbilityからの操作でRemoveした場合、自動的にその時点でActorがDestroyされるように設定します。場合にもよると思いますが、AbilityからはAdd、Execute、Removeの三種類の動作しか行えないため、多くの場合Trueにされることが多いです。

AutoAttachToOwnerはチェックするとこのActorがOwnerであるActorにAttachされるようになるオプションです。なんでこの項目があるのかというと、例えばOwnerActorにパーティクルをAttachしようとした場合。

f:id:wvigler:20210502103957p:plain

こんな形でOwnerActorのRootComponentに直接EmitterをAttachすることが考えられます。これはできることはできるのですが、問題が発生します。

UE4において外部ActorからAttach済みのComponentを操作、削除するのは制限が掛けられており、GameplayCueNotify_Actorからでは一度発動したEmitterの動作を操作したり停止させることができなくなってしまいます。

これではHandlerの意味がないので、こういう場合は…

f:id:wvigler:20210502104632p:plain

自身のRootComponentにEmitterをAttachし、自分自身をOwnerActorにAttachすれば、操作も可能で、削除する際はComponentをDestroyすれば削除されます。

では実際に炎のエフェクトを追加していきましょう。

…と思ったけど、ちょっと作成してみたところ意外に複雑だったので、まずは追って説明はしないで完成した品を見てもらいます。

<GQA_Flame-OnActive>

f:id:wvigler:20210503054211p:plain

f:id:wvigler:20210503054412p:plain

<GQA_Flame-OnRemove>

f:id:wvigler:20210503054554p:plain

<GQA_Flame-ClassDefaults>

f:id:wvigler:20210503062603p:plain

<BP_GASCharacter-BeginWeaponExpand>

f:id:wvigler:20210503065206p:plain

<GPA_ExpandWeapon>

f:id:wvigler:20210503065402p:plain

f:id:wvigler:20210503065512p:plain

f:id:wvigler:20210503065542p:plain

実行画面<通常時>

f:id:wvigler:20210503065730p:plain

実行画面<武器拡大時>

f:id:wvigler:20210503065907p:plain

f:id:wvigler:20210503070203p:plain

基本構造ですが、Payload(GameplayEvent)とParametersどちらも使用しています。Payloadは武器拡大のTimelineが終わった際にそのDirectionをfloatに変換した形でAbility側に通知し、それによってCueをActiveにするのかRemoveするのかを決定します。

ParametersはAbilityからSkeletalMeshのComponent情報をCueに通知し、CueはそこからBone情報を読み取って"hand_r"にHandlerであるActorをAttachします。これで"hand_r"のBoneから炎が発生するようになりました。OnRemove側の実装はオートでDestroyされるので一見必要ない用に見えますが、どうやらComponent情報はDestroyされても保持されているらしく(もしかしたらHandlerはプーリングされる仕組みになってるのかも)、これがないと繰り返す度にどんどん炎が強くなってしまうので入れています。

 

どうでしたでしょうか?個人的にはGameplayCueは構造自体はそこまで難しい話ではないのですが、まだ整理されきっていないのか罠や落とし穴が多くて、そこに嵌ると(情報量が少ないのも相まって)なかなか抜け出せないな、と感じました。

しかし、共通のエフェクトや効果音を一つにまとめて管理できるため、間違いなく有能なシステムではあるので、頑張って身につける価値はあると思います。

 

・あとがき

この記事群についてですが、「GameplayAbilitySystemについて包括的な情報がないから(ぷちコンである程度使ったし)自分で書こうかな?」みたいな軽い気持ちで初めました。

しかし、調べていくうちに次々新しい要素が見つかり、それも書かなきゃ、みたいなことが重なり、結局全体で3万字を超えるという代物になりました。

あんまり長すぎると読む人が苦痛じゃないかと心配になってくるわけですが、かといって備忘録の役割もあるので省くのもなぁ、というジレンマもありこのぐらいの長さになっています。(これでも一部省いてはいます。最初はAIへの導入とかも書くつもりだった)

注意ですが、これでもGameplayAbilitySystemは全然網羅しきれてません。今回Targetingやネットワーク関連は全く手を付けていませんが、本来GameplayAbilitySystemはここで威力を発揮するシステムなのです。なので僕が把握しているのは狭い狭い範囲に過ぎません。

GameplayAbilitySystemの学習での一番の壁はその膨大さだと思います(多分二番は情報の少なさ)。これを書く前は僕も「なんか色々資料はあるけれど分かりにくな…ActionRPG関連も概念の説明ばっかりだし…」と思ってたんですが、書いてみると膨大すぎて「これを分かりやすく書くのは無理だわ…」となりました。適用範囲も書く前より大分広がって見えていて、「むしろこのシステムが適用できないゲームって何かあるかな?」と思っています。

ただGameplayAbilitySystemはBPからUC++への導入として考えるとかなりいいのではないかと思います。ある程度決まった書き方ができれば導入できる感じなので、「全てBPで書くと把握しきれないぐらい自分の制作しているゲームが複雑化してきた」「でもUC++使うのはまだ怖いし、どこに使ったらいいのかも分からない」というぐらいの製作者が導入するのにぴったりで、しかもちゃんと効果的であるというシステムなのではないでしょうか?

僕も今回のこの記事を書いてネットワーク関連を全く学んでいなかったことを再認識して、次はこれを中心軸にして学ぼうと目標を立てることができました。ネットワーク頑張るぞ!…とりあえずEpic公式のMultiplayerのチュートリアル見るか…

 

そんなこんなで第15回ぷちコン振り返りでした!ありがとうございました!

というか3月締切のぷちコンの振り返りが5月にやっと公開というね…

 

ご指摘、ご質問、ご意見などあればコメントにお願いします。

第15回ぷちコン「たぬ吉の大冒険」振り返りその3<GameplayEvent、GameplayTask編>

注》この記事はこの記事の続きです。

この記事では第15回ぷちコンの振り返りとして、「GameplayEvent」と「GameplayTask」の解説をしていきます。

 

お品書き

・GameplayEventについて

・GameplayTaskについて

・GameplayEvent、GameplayTaskを実装してみる

注》今回解説する部分は結構ネットワーク分野と関連しているため、自分でも理解できていない部分が比較的多いです。ご容赦ください。 

 

・GameplayEventについて

GameplayEventはGameplayAbilitySystemにおいてGameplayTagを使用した特殊な使い方をできるStructureです。GameplayEventの情報はよくPayloadと呼ばれ中身はこうなっています。

f:id:wvigler:20210501033754p:plain

EventTagはGameplayEventにおいて最も中心的な役割を持つGameplayTagです。これについては後で解説します。

そして実は以下の項目全ては一応の型は決められているものの、どう使うかはユーザーに委ねられています。例えばInstigatorというActor型の項目があります。攻撃などの時は例えば攻撃する側がここに登録されることが多いし、その方が問題も起きにくいと思いますが、しかし攻撃される側を登録しても全く影響はありません。このデータ型をどのように使うかはほぼ完全に使用者次第となっています(少しだけ例外はありますが…)。そしてもちろん普通のVariable変数として扱うことも可能です。

f:id:wvigler:20210501042712p:plain

さて、問題は一番上のEventTagでして、これだけは少し毛色が違います。GameplayEventは基本的にSendGameplayEventToActorというノードを介して扱います。

f:id:wvigler:20210501042218p:plain

SendGameplayEventToActorは特定のActorにGameplayEventの情報を送るものですが、EventTagに特に何の指定もしていない場合、EventTagにはSendGameplayEventToActorのEventTagが入ります。ではActorに送ったPayloadですがその受け取りはどのような形で行われるのでしょうか?

使い方の一つはGameplayAbilityのActivateです。そう、GameplayEventからGameplayAbilityをActivateすることができます。それではこれから"GAS_Test"プロジェクト内でZキーを押すとActivateしていた攻撃を今度はGameplayEventからActivateさせられるようにしてみましょう。

f:id:wvigler:20210501043500p:plain

まずGASCharacterのEventGraph内です。元の状態はこうなっています。これを…

f:id:wvigler:20210501043804p:plain

このようにします。Payloadはとりあえず何も入れなくて大丈夫です。もちろんこれだけではZキーを押しても何も起こりません。次はGPA_Attackに移動します。

f:id:wvigler:20210501044023p:plain

さあこれが元の状態です。まずは先頭のEventActivateAbilityを削除します。このノードがあると絶対にGameplayEventによるActivateは起こらないようになっています!必ず削除してください!そして今度はEventActivateAbilityFromEventというノードを同じように配置します。

f:id:wvigler:20210501044342p:plain

しかしまだ何も起こりません。GameplayEventによるActivateを引き起こすには更にClassDefaultsのTriggersカテゴリのAbilityTriggersを追加し、TriggerTagにキーとなるEventTagを設定します。それから、これは初期状態でそうなっているので問題ないかと思いますが、TriggerSourceはGameplayEventにしておきます。

f:id:wvigler:20210501045031p:plain

さあ、これで準備が整いました。実行してZキーを押してみましょう。以前と同じように攻撃が発動するはずです。この方法の利点はPayloadによって多くの情報をAbilityに持ち込める点にあるでしょう。

f:id:wvigler:20210501045640p:plain

これだけの情報をActorからAbility内に持ち込めます。また、この方法でAbilityを呼び出すと、AbilityのClassDefaults->TagsカテゴリにあるSourceRequired/BlockedTagsとTargetRequired/BlockedTagsに意味が生まれます。

f:id:wvigler:20210501050139p:plain

PayLoad内にInstigatorTagsとTargetTagsという項目がありますが、これがつまりAbility内におけるSourceTagsとTargetTagsですRequiredの場合はInstigatorTagsやTargetTagsにそのTagが登録されていなければ発動せず、逆にBlockedの場合はInstigatorTagやTargetTagsにそのTagが登録されていれば発動しません。

GameplayEventについてはWaitGameplayEventというノードを使う活用方法もあるのですが、WaitGameplayEventはGameplayTaskに属するノードなので、次はGameplayTaskについて解説します。

・GameplayTaskについて

正直良く分かってないんだ。

GameplayTaskはGameplayAbilitySystem内で非同期処理を実現するための仕組みで、特にAbility内のみで呼ばれるものはAbilityTaskと呼ばれます。というか自作しない限り全部これです。

Taskは自作することもできますが、すでにかなりの数がノードとして実装されています。GameplayAbility内で"wait"で検索してみると多さが分かるでしょう。まあDelegateのようなことをするものだということは分かるし、名前である程度使い方が分かるものも多いですが、Targeting関連の部分は正直自分にはよく分かりませんし、更に言うと…

f:id:wvigler:20210501053104p:plain

この2つのノードの違いは何かと聞かれても、僕には説明できません。AbilityTaskノードには全て付いていますが、AsyncTaskのPinが付いている理由も分かりません。繋がるノードは多いようなので、恐らく何かできるんでしょう(適当)。

自作についてはえーっと…(目が泳ぐ)…あー…

ま、とはいえある程度使い方が分かるノードも存在しています。今回はその中でGameplayEventの情報を受け取るWaitGameplayEventを使用していきます。

・GameplayEvent、GameplayTaskを実装してみる

今まで作成してきたAbilityの中身はおおよそこんな感じでした。

f:id:wvigler:20210501055127p:plain

PlayMontageにいろいろなExecutionPinが付いているので、EndAbilityを繋げることができていますが、ではこういったPinが無かった場合どうすればいいでしょう?

具体的にはTimelineを使用してアニメーションを作り、Timelineが終了したらEndAbilityしたいのです。しかしTimelineはAbilityでは扱えずActor内での実装になります。まあTimelineを発動するだけなら特に問題にはならないのですが、問題は発動したTimelineの終了をどうAbilityに知らせるか、です。現状でこれを通知する手段がありません。

今回はGameplayEventとGameplayTaskであるWaitGameplayEventを使用してこの通知を行い、「武器が大きくなる」というTimelineを使用したアニメーションを作成します。

まずは下準備として装備した武器の大きさを小さくします。

f:id:wvigler:20210501060218p:plain

0.5倍にすればまるでナイフのようになります。それではTimelineを作成しましょう。

f:id:wvigler:20210501094348p:plain

Timelineの中身はこうなっています。

f:id:wvigler:20210501094959p:plain

新しいAbilityを作成します。名前はGPA_ExpandWeaponとしました。

f:id:wvigler:20210501070601p:plain

f:id:wvigler:20210501073407p:plain

とりあえずノードをここまで組みます。本来はBPInterfaceを用意した方がいいのですが、面倒なのでCastしました。続いてWaitGameplayEventを使用し、

f:id:wvigler:20210501081212p:plain

このようにします。Payloadに情報を乗せることはできますが、この場合は必要ありません。Tagsカテゴリはこのように…

f:id:wvigler:20210501082903p:plain

これをAbilityListに登録して…

f:id:wvigler:20210501091700p:plain

CキーでActivateするようにします。

f:id:wvigler:20210501094048p:plain

そしてTimelineが終了したらSendGameplayEventToActorで"Event.OnTimelineFinished"のTagを自身に送るようにします。

f:id:wvigler:20210501094739p:plain

実行してみると…

f:id:wvigler:20210501095059p:plain

Cキーを押す度に剣が大きくなったり小さくなったりしています。このようにActor内のTimelineの状態を取得するのにGameplayTask、GameplayEventは使用できます。

 

ふー、GameplayTaskとGameplayEventは比較的楽でしたね。というかTaskについてはまだ分かっていないことが多くて下手なこと書けないというのはありますが…ネットワークをうまく使えればGameplayTaskをC++でゴリゴリ自作するなんてことになるんでしょうか?

 

というわけで次回はGameplayCueです。やったー!最後だ!

 

ご指摘、ご質問、ご意見などあればコメントにお願いします。

 

続きの記事はこちら

wvigler.hatenablog.com

第15回ぷちコン「たぬ吉の大冒険」振り返りその2<GameplayAttribute、GameplayEffect編>

注》この記事はこの記事の続きです。

この記事では第15回ぷちコンの振り返りとして、GameplaySystemのうちGameplayAttributeとGameplayEffectを解説します。

 

お品書き

・前回の手直し

・GameplayAttributeについて

・AttributeSetについて

・GameplayAttributeを導入してみる

・GameplayEffectについて

・GameplayEffectを導入してみる

・GameplayModMagnitudeCalculationについて

・GameplayEffectExecutionCalculationについて

 

・前回の手直し

前回作成したプロジェクトなのですが、今回解説する内容と合わせるために若干手を入れようかと思います。まず前回作成した「GPA_Test」の名前を「GPA_Attack」に変更します。

f:id:wvigler:20210425000518p:plain

次に攻撃にCollisionを付けます。

BP_GASCharacterにBoxCollisionを追加します。

f:id:wvigler:20210425001712p:plain

こんな感じで。そうしたらCollision設定をこうして…

f:id:wvigler:20210425005348p:plain

次にBoxCollisionにComponentTagを付けます。名前は「Attack」としておきます。

f:id:wvigler:20210425002321p:plain

次にAnimationNotifyを作成します。

f:id:wvigler:20210425002800p:plain

名前は「NF_ActivateAttack」としておきます。

f:id:wvigler:20210425003208p:plain

RecievedNotifyをOverrideして…

f:id:wvigler:20210425010522p:plain

f:id:wvigler:20210425010601p:plain

このようにノードを組みます。要は「Attack」のTagが付いているBoxCollisionにOverlappingしているBP_GASCharacterのダメージAbilityを発動させる、ということです。これを…

f:id:wvigler:20210425011211p:plain

攻撃モーションのこの辺りに設置します。

これでBP_GASCharacterをThirdPersonExampleMapに新たに置くと…

f:id:wvigler:20210425011542p:plain

このように攻撃すると相手がのけぞるようになります。後でまた弄りますが、インタラクションがあったほうが楽しいので一旦こうしておきます。今回はここからスタートです。

・GameplayAttributeについて

GameplayAttributeはGameplayAbilitySystem内で個々のActorの持つ様々なパラメーター(HPやMP、攻撃力、防御力など)をfloat値として統括管理するシステムです。ゲームにはこれらの数値の管理が殆どの場合必須です。

GameplayAttribute自身はFGameplayAttributeDataという変数型で提供されています。AttributeはBaseValueとCurrentValueという2つの数値を持っており、このどちらにもアクセスできるようになっています。

GameplayAttributeを扱うにはAttributeSetというC++クラスが必要です。

・AttributeSetについて

AttributeSetはGameplayAttributeを定義し、それを扱うための関数やマクロを提供するためのC++クラスで、これをASCに付与することでAttributeにアクセスすることができるようになります。また、AttributeSetは複数違うものを持つことも実は可能です。

AttributeSetにはいくつかの仮想関数が定義されています。これらは重要なので少し見ていきましょう。

>void PreAttributeChange(const FGameplayAttribute& Attribute, float NewValue)

>void PreAttributeBaseChange(const FGameplayAttribute& Attribute, float NewValue)

Attribute(BaseValue or CurrentValue)が書き換えられる直前に処理が走ります。Clamp処理を掛けたりする際に使用します。

>bool PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data)

GameplayEffectによりAttributeに変更が入る直前に処理が走ります。GameplayEffectの処理自体を一定条件で拒否したりといった用途に使用します。

>void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)

GameplayEffectによりAttributeに変更が入った直後に処理が走ります。Actorへの通知は基本的にここで行うことが推奨されているので、おそらく一番使う機会が多いと思います。

・GameplayAttributeを導入してみる

なにはともあれAttributeSetのC++クラスを作成しましょう。

f:id:wvigler:20210425013854p:plain

名前は「GASAttributeSet」としておきましょう。

f:id:wvigler:20210425015947p:plain

エディタを開いてGASAttributeSet.hに書き込んでいきます。

まずはAbilitySystemComponent.hをincludeします

f:id:wvigler:20210425041311p:plain

続いてアクセサマクロを設定します。

f:id:wvigler:20210425041414p:plain

これはGameplayAttributeからfloat値の取り出しやfloat値による書き換えをする際に使用します。

次にそれぞれのGameplayAttributeを宣言します。宣言するのはHealth、MaxHealth、AttackBase、AttackMultiplier、DefenseMultiplier、そしてDamageです。Publicに記述することを忘れないようにしましょう。(忘れるとコンパイルエラーが出ます)

f:id:wvigler:20210425033308p:plain

ここでちょっと疑問に思うかもしれません。「Damageってパラメーターなの?」と。

DamageはMetaAttributeと呼ばれるもので、一般的なパラメーターではなく、ダメージ計算時に使用するものです。非常に便利なのですが、あとでGameplayEffectExecutionCalculationのところで具体的な使い方を説明します。

ついでにPostGameplayEffectExecuteを宣言します。

f:id:wvigler:20210425042340p:plain

今の所中身は空ですが、cppファイル側で一応定義はしておきます。

f:id:wvigler:20210425042600p:plain

 さて、お次はGASCharacterクラスを弄っていきます。エディタを開いて、まずはGASCharacter.hでGASAttributeSet.hをincludeします。

f:id:wvigler:20210425075841p:plain

宣言に使用するUPROPERTYマクロは引数なしで構いません。アクセスはPublicで。

f:id:wvigler:20210425080503p:plain

追加の仕方は通常のコンポーネントと全く変わりません。しかし、もちろんコンポーネントではないのでBP側には何も表示されません。

f:id:wvigler:20210425080735p:plain

とりあえずC++側はこれで終わりです。コンパイルしましょう。

さて、終わったらBP_GASCharacterの中身を見ていきます。

f:id:wvigler:20210425082118p:plain

ダメージモーションは確認したのでもうこのノードは要らないです。なので…

f:id:wvigler:20210425082428p:plain

Attributeにアクセスするためにアクセサを介した独自ノードをActorに実装する方法もあるのですが、個人的には必要無いと思います(分かりやすいので説明や習熟するためとしてはアリだと思いますが)。

上のようにAttributeをGetしたい時にはGetFloatAttributeFromAbilitySystemCompoentかGetFloatAttributeBaseFromAbilitySystemComponent、Setしたい時にはGameplayEffectを使用すれば問題ないと思います。

じゃあそもそもアクセサ要らないじゃん、というとそういうわけでもなく、C++内の関数で結局必要になったりします。

それからAbilitySystemが更新されているので、EventGraphのAbilitySystemノードを繋ぎ直す必要が出てきます。これは前の記事内でAbilitySystemを直接参照できるようにした弊害なのですが、まあこのぐらいは許容範囲とします。

それはさておき、実行してみましょう。

f:id:wvigler:20210425083336p:plain

まだ何も弄っていないので数字は0です。ただこのノードはアクセスに失敗しても0を返すため、数字だけでは成功しているかが不明です。しかしBooleanがtrueになっているため、たしかにアクセスできていることが分かります。

さて、GameplayAttributeによってキャラのパラメーターを用意することに成功しましたが、パラメーター自体を弄ることがまだできていません。それをするために必要なのがGameplayEffectです。

・GameplayEffectについて

さあGameplayEffectは長いぞ。

GameplayEffectはAttributeに対し影響(Effect)を与えるものです。しかしそれに留まらず、めちゃくちゃ色んな事ができるようになっています。基本的にこのクラス自体はC++での実装は要らず、BPのみで取り扱えます。とりあえずGameplayEffectのBPクラスを作成して、中身を覗いていきましょう。

f:id:wvigler:20210425085537p:plain

まずは初期化に使用したいので、名前は「GPE_Initialize」とします。

f:id:wvigler:20210425085817p:plain

中身を開くとこのようになります。

f:id:wvigler:20210425090017p:plain

GameplayEffectクラスでは弄るのはDetailsのみなので、左側は一切必要ありません!

そして肝心の右側には…

f:id:wvigler:20210425090257p:plain

うんざりするぐらい色々並んでいます。これらを全て解説すると記事がもう2、3個必要になってきりがないので、必要なところだけを解説します。(ここでだいたい書かれているので、気になる人は見てください)まずは一番上の「GameplayEffect」カテゴリです。

f:id:wvigler:20210425090621p:plain

DurationPolicyはInstant、HasDuration、Infiniteの三種類があり、それぞれEffectをどのぐらいの期間適用するかを決定します。Instantは即時で一回だけ。HasDurationは一定期間ずっと。Infiniteは永久に適用します。またこれはAttributeのBaseValueとCurrentValueを同時に変更するのか?それともCurrentValueのみ変更するのかに関わってきます。前者がInstantであり、後者がHasDurationとInfiniteです。

またこの選択によって下のカテゴリである「Period」が変化します。

[Instant]

f:id:wvigler:20210425091715p:plain

[HasDuration] or [Infinite]

f:id:wvigler:20210425091814p:plain

PeriodはEffectを適用する周期です。1/60とか1/24とか設定することが多いですが、0のまま使用することもあります。

ExecutePeriodicEffectOnApplicationはオンにするとEffectが適用された瞬間にAttributeが書き換わり、その後Period秒毎にAttributeが再び書き換わります。オフにするとEffectが適用された瞬間には書き換わらず、その後Period秒後に初めてAttributeが書き換わります。

PeriodicInhibitionPolicyは何らかの理由でEffectが中断された場合、NeverResetの場合は中断された場所から何事もなかったかのように再び適用が始まります。ResetPeriodの場合、Periodが一旦リセットされ、再開すると最初にEffectが付与された時と同じように振る舞います。ExecuteAndResetPeriodの場合、基本的にはResetPeriodと同じですが、中断された際に一度だけEffectが適用されます。

それからHasDurationを選択した場合は「GameplayEffect」カテゴリにDurationMagnitudeという項目ができます。

f:id:wvigler:20210425101102p:plain

これはEffectの適用期間を決定するものです。

さてお次の項目はModifiersです。配列になっているのでちょっと追加して覗いてみると…

f:id:wvigler:20210425101855p:plain

上から解説していきます。AttributeはEffectによって変更するTarget(GameplayEffectのSourceとTargetについては後で説明します)のAttributeを選択します。

f:id:wvigler:20210425113525p:plain

現在はこんな風に選択できます。

ModifierOpはAttributeに対する計算方法です。

f:id:wvigler:20210425113843p:plain

これは説明不要ですね。マイナスの値をAddに入れると数値の減少を表現することができます。

MagnitudeCalculationTypeは変更する元となる数値を決定します。

f:id:wvigler:20210425122839p:plain

ScalableFloatは直接入力で数値を決定します。FCurveFloat型です。

f:id:wvigler:20210425123539p:plain

AttributeBasedはSource又はTargetのAttributeを参照して数値を決定します(ある程度は変更もできます)。

f:id:wvigler:20210425123912p:plain

CustomCalculationClassはGameplayModMagnitudeCalculationクラスを使用して数値を決めます。MagnitudeCalculationTypeの中では最も自由度が高くなりまが、C++での実装が必須となります。

f:id:wvigler:20210425124520p:plain

最後にSetByCallerです。これは見てみた方が早いので(そして初期化に向いているので)、実装しながら見ていきましょう。

…と言いたいところですが、まだ一つ説明することがあります。GameplayEffectの適用方法についてです。

GameplayEffectは基本的に以下のようなノード構成で適用します。

[AbilitySystemComponentから適用する場合]

f:id:wvigler:20210425132816p:plain

f:id:wvigler:20210425133106p:plain

見ての通りAbilitySystemComponentからSpecHandleを作成して、それをノードに送って適用する、という流れです。そしてGameplayEffectにはASCに対してSource(適用する側)とTarget(適用される側)という区別があります。上の2つの例の場合、どちらも上側のASCがTarget側、下側がSource側になります。(この場合参照しているのは全く同じASCではありますが…)

[GameplayAbilityから適用する場合]

f:id:wvigler:20210425133616p:plain

f:id:wvigler:20210425133804p:plain

GameplayAbilityから適用する場合、Sourceは強制的にAbilityを所持している側のASCになります。そして上の例はとても単純にTarget側もAbility所持者のASCが担当している、ということなのですが…下側はTargetingというシステムが絡んでいます。これは名前こそ同じですがGameplayEffect内のTargetの概念とはまた違うものでして…一応オフラインでも用いることはできるものの、上手く活用するためにはネットワークが絡んでくるみたいなので、僕もまだ良くわかってません。なので触れないでおきます。

・GameplayEffectを導入してみる

さて、やりたいことは各AttributeのInitializeです。それにはどのMagnitudeCalculationTypeが最適でしょうか?

ScalableFloatとAttributeBasedは便利な場面ももちろんありますが、基本的にBP側のfloat値を参照できません。例えばパラメーターをInitializeする場合、キャラクターのレベルに応じた数値をDataTableから引っ張ってくることやBPエディタに公開したVariableを参照する方法が考えられます。その場合BPのfloat値が使えないと数値を持ってこれません。CustomCalculationClassは最も自由度が高い方法なので、もちろんBPの数字を持ってくることができますが、C++での実装が必須になるのでお手軽ではありません。というわけで、この場合Initializeに最適なのはSetByCallerとなると思います。

SetByCallerにはAssignTagSetByCallerMagnitudeというノードを使用します。AssignSetByCallerMagnitudeというノードもありますが、こちらはどうやらC++無しでは今の所機能しないようです。

f:id:wvigler:20210425232229p:plain

このように繋げると…

f:id:wvigler:20210425232516p:plain

Tagとfloatが紐付けられた情報であるSetByCallerがSpecにAssignされます。EventBeginPlayノードにこれをどんどん繋げて…(初期値はVariable化しています)

f:id:wvigler:20210426031344p:plain

GPE_Initializeの方もずらずらずらっと全部SetByCallerで…

f:id:wvigler:20210426032948p:plain

f:id:wvigler:20210426032913p:plain

Xキーを押したら可視化できるようにしていきましょう。

f:id:wvigler:20210426032707p:plain

f:id:wvigler:20210426032756p:plain

これで実行中にXキーを押すと…

f:id:wvigler:20210426033111p:plain

このようにGameplayEffectを介してAttributeを変更することができたことが分かります。

・GameplayModMagnitudeCalculationについて

最も自由度が高いMagnitudeCalculationTypeであるCustomCalculationClassにおいて計算に使用するのがGameplayModMagnitudeCalculation(以下GMMC)クラスです。これはBPでは中身を操作できずC++実装オンリーのクラスで、その本体はCalculateBaseMagnitude_Implementation(const FGameplayEffectSpec& Spec) constというfloat値を返す関数です。他のMagnitudeCalculationTypeでできる全てのことが可能で、かつそれらの値を使用した複雑な計算式や条件式を作成することができます。

ではGMMCを使用した攻撃ダメージ処理を作っていきましょう。GMMCの派生クラスを作成します。

f:id:wvigler:20210426043746p:plain

名前は「GMMC_Attack」としました。

f:id:wvigler:20210426044702p:plain

ヘッダを開いて、まずは宣言から…

f:id:wvigler:20210426051912p:plain

コンストラクタとAttribute情報を格納するためのFGameplayEffectAttributeCaptureDefinitionという型の変数を3つ用意します。それから本体となるCalculationBaseMagnitude_Implementationですね。

次はcppファイルを開きます。まずAttributeの参照に必要なAttributeSetクラスをincludeします。

f:id:wvigler:20210426053117p:plain

次にコンストラクタでSource側のAttackBase及びAttackMultiplier、Target側のDefenseMultiplierをCaptureします。

f:id:wvigler:20210426055412p:plain

SnapshotをtrueにするとEffectSpecが作成された瞬間、falseにするとそのSpecがGameplayEffectによって適用された瞬間にAttribute情報がCaptureされます。活用例としてProjectileのActorなどが分かりやすいですが、射出時に射出側のActorでAttributeをSnapshotしてSpecを作成し、ProjectileはそのSpecの情報のみを保存して運び、被弾側のActorに至って初めてそのSpec情報を参照してGameplayEffectとして適用すれば、ProjectileのSpecが持っているのは射出時点のAttribute情報となります(逆にSnapshotしていなければ被弾時点でのAttribute情報が参照されます)。はっきり言って、このようなMeleeな攻撃では効果を実感しづらい代物ですが、一応Source側はtrue、Target側はfalseにしておきます。

次はCalculationBaseMagnitude_Implementationを弄っていきます。

f:id:wvigler:20210426062125p:plain

まずはSourceとTargetのTagを集め、EvaluationParametersに投入します。TagによってはCaptureした数値が(PreAttributeChangeなどにより)変化する可能性があるからです。そして用意したLocal変数にGetCaptureAttributeMagnitudeを介して値を代入していき、最終的に計算を行ってその値を返り値にします。(除算がある場合はくれぐれもゼロ除算に注意しましょう)

ここまで書いたらコンパイルします。

次はBP側です。新しいGameplayEffectの派生BPクラスを作成します。名前は「GPE_Attack」とします。

f:id:wvigler:20210426070751p:plain

Detailsはこのようにします。

f:id:wvigler:20210426071858p:plain

Coefficientが-1.0になっていることに注意しましょう。

次にNF_ActivateAttackの中身を書き換え、TryActivate~の直前にGPE_Attackを適用します。

f:id:wvigler:20210426072727p:plain

と、これで一応プログラム上は問題ありませんが、可視化されていないため本当にEffectが動いているのかどうかが分かりません。なので様々な改良も含め、また少しプロジェクトを弄ります。

 

いきなりですが、まずはここからTryActivateAbilitiesByTagを削除します。

f:id:wvigler:20210426074139p:plain

次にGASCharacter.hを開き、Publicに以下のようにOnDamaged(float Damage, float Health, AActor* Attacker)とOnDied(float Damage, AActor* Attacker)という関数を宣言します。

f:id:wvigler:20210426075120p:plain

これは宣言のみで、cppファイル内に定義を書く必要はありません。

次に弄るのはGASAttributeSet.cppです。以前殆ど宣言しただけで放置したPostGameplayEffectExecuteの中身を書いていきます。

f:id:wvigler:20210427045655p:plain

まずは先程のOnDamagedやOnDiedを扱うためにAGASCharacterクラスへのアクセスが必要なので"GASCharacter.h"をincludeします。様々なGameplayEffect関連の情報を扱う必要もあるため、"GameplayEffect.h"と"GameplayEffectExtension.h"もincludeしておきましょう。そうしたらEffect処理後の処理を書いていきます。

f:id:wvigler:20210427061535p:plain
ここでやっていることはまずはTargetのActorとSourceのActorを取得して、それからEffectによってHealthの数値に変更があった場合に、Healthの変更がマイナス方向で、かつ変更後の数値が0以上ならTargetのOnDamagedを呼び出し、0以下ならOnDiedを呼び出す処理です。その後にDamage(前にAttributeSet内に定義したMetaAttribute)を0にし、最後にHealthをClamp処理して終了します。

さてC++で弄るのはここまでです。コンパイルしましょう。

これからBP側を弄るのですが、下準備としてダメージを受け続けて死んだ時用のAbilityを作成します。

f:id:wvigler:20210429233250p:plain

Animationの用意が面倒なのでPhysicsAnimationにしました。

f:id:wvigler:20210429233346p:plain

 

f:id:wvigler:20210427131616p:plain

TagはAbility.Deathで呼び出せるようにしました。

そして今度はダメージリアクションのAbilityをキャラクターに設定します。まずBP_GASCharacterを開きます。BlueprintImplementableEventで宣言したので、BP_GASCharacterのEventGraphでは"OnDamaged"と"OnDied"がEventとして呼べるようになっています。

f:id:wvigler:20210427125451p:plain

それぞれ以下のように繋ぎます。

f:id:wvigler:20210427141225p:plain

f:id:wvigler:20210427141305p:plain

DamageとDeath、それぞれリアクションとしてのAbilityの呼び出しと、パラメーター情報の表示をさせます。

f:id:wvigler:20210429234635p:plain

最後にAbilityListにAbilityを登録するのを忘れないようにしましょう(よく忘れる)。

次に新たなGameplayEffectを作成します。名前はGPE_ConvertDamageToHealthとします。

f:id:wvigler:20210428121419p:plain

中身はDamageの数値をマイナス方向のHealthに変換するだけです。AttributeSourceをTargetにするのを忘れないようにしましょう。

f:id:wvigler:20210428143242p:plain

GPE_Attackも少し直します。

f:id:wvigler:20210427083628p:plain

書き換えるAttributeをDamageに、Coefficientを1.0に変更しました。

そして…

f:id:wvigler:20210428150205p:plain

GameplayEffectsカテゴリのConditionalGameplayEffectsにGPE_ConvertDamageToHealthを追加します。ConditionalGameplayEffectsはGameplayEffectが成功した場合にここに登録されたEffectが続いて発動します。これはEffectで変更したAttributeを、変更した後に再びEffectで使用したい場合などに使用します。基本的にAttributeのCaptureは一度しか行われず、一つのEffect内では変更後のAttributeを扱うことができません。しかしEffectを何度も呼び出すのもノードが増えてスマートではない(更に言うと何度も書くのがそもそも面倒)のでこのような項目が用意されています。

基本構造としてはBP_GASCharacterがGPA_Attackを発動した際に、AttackCollision内にいる他のBP_GASCharacterにGPE_Attackを適用、続いてGPE_ConvertDamageToHealthが適用され、それがHealthに変換されます。Healthが変更されたのでPostGameplayEffectExecuteからTargetActorのEventの発動やHealthのClampが行われ、最後にDamageの値が0になって次の攻撃に備える、となります。

 

なぜDamageというMetaAttributeを用意したかというと、こうしてHealthとDamageを一旦切り離すことでBuff、Debuffやアイテムなどによる数値の変更がやりやすくなる、という効果があります。例えば防具などを装備しているとある程度のダメージを肩代わりしたり、ダメージを割合で減らしたりする場合、MetaAttributeが無く直接Healthを減算する形とすると、後のEffectなどでもともとのダメージ値が参照できなくなってしまい、効果を手軽に追加・削除できなくなってしまいます。小さなプロジェクトなら全く問題が無い場合もあるのでケースバイケースではあるのですが、この使い方自体は覚えておいたほうがいいと思います(GameplayAbilitySystem外でも有効です)。

またMetaAttributeを複数用意することも有用です。このプロジェクトでは現在毒や火傷などといったDOT系統のダメージを実装すると少々問題が起こります(Healthの変更が行われる度にAnimationが再生される仕様なので)。これは新たに「DOTDamage」などといったMetaAttributeを作成し、PostGameplayEffectExecuteを書き換えると解決するのですが、ここでは今は触れないでおきます。

さて、ここまでGMMCの実装について書いてきましたが、実はGameplayEffectにはもっと自由度の高い計算方式があります。それがGameplayEffectExecutionCalculationです。

・GameplayEffectExecutionCalculationについて

GameplayEffectExecutionCalculation(以下GEEC)はGameplayEffect内で最も自由度の高い計算方式です。GMMCでできることに加えて、AttributeからCaptureする数値自身をBPから操作することができます。

今回はこれを使用してHP吸収攻撃を作成していきたいと思います。

まずはGEECクラスを作成していきましょう。ちなみにこちらもBPでの実装はできず、C++オンリーのクラスとなっています。

f:id:wvigler:20210430013336p:plain

名前はGEEC_DrainAttackとします。

f:id:wvigler:20210430014951p:plain

GEEC_DrainAttack.hに宣言を書き込みます。宣言するのはコンストラクタとExecute_Implementationという引数がやたらに長い仮想関数です。

f:id:wvigler:20210430052106p:plain

今度はGEEC_DrainAttack.cppに移動し、まずは"GASAttributeSet.h"と"AbilitySystemComponent.h"をincludeします。

f:id:wvigler:20210430053318p:plain

次にcpp内に"DrainStats"というStructureを作成します。GEECにはCaptureについてのマクロが提供されているのでカンタンです。

f:id:wvigler:20210430094353p:plain

DEFINE_ATTRIBUTE_CAPTUREDEFの引数は左から順にクラス名、Attribute名、Target or Source、Snapshotとなっています。

次にコンストラクタです。

f:id:wvigler:20210430094825p:plain

Structureとマクロを使用しているだけで、やっていることはGMMCと殆ど変わりません。

続いてExecute_Implementationです。一気に完成形までやっていきます。

f:id:wvigler:20210430124021p:plain

f:id:wvigler:20210430124103p:plain

GMMCと引数や関数の名前が変わっていますが、ここでもやっている事自体は前回とあまり変わりません。変わったことは、RecoverMultiplierというSetByCallerを参照し、Damageに掛けたものでSourceを回復させている点です(回復方法はあまりスマートじゃありませんが…)。このようにGMMCやGEECではSetByCallerから数値を参照することもできます。

さて、これをコンパイルして…

終わったら今度はBP側です。GPE_DrainAttackというGameplayEffectを作成します。

f:id:wvigler:20210430130347p:plain

中身はこうします。

f:id:wvigler:20210430153312p:plain

ここでは使用しませんが、CalculationModifireを追加するとCaptureする値にModifierの項目と同じような変更を施すことができるようになります。これはGEECの強みの一つで、実行結果を見ながら少しづつ係数を上げたりといった数値の微調整をする場合などに役に立ちます。

f:id:wvigler:20210430155851p:plain

Abilityの作成の仕方はもうやっているので省略しまして、NotifyはNF_ActivateAttackをDuplicateしたところから…

f:id:wvigler:20210430160104p:plain

こうなってる部分を…

f:id:wvigler:20210430161216p:plain

こんな風に変更します。

それからちゃんと機能しているか可視化するために自身のHealthを減らす仕組みも必要ですね。"GPE_ReduceHealth"というGameplayEffectを用意しまして…

f:id:wvigler:20210430234101p:plain

一気に80減らします。

最終的に…

f:id:wvigler:20210430235356p:plain

ZキーとXキーで通常攻撃と吸収攻撃…

f:id:wvigler:20210430235626p:plain

CキーでHealthを減らして…

f:id:wvigler:20210501000349p:plain

Qキーを押すとステータスを表示するということにしました。

実行してみましょう。

f:id:wvigler:20210501005354p:plain

実行時にQキーを押すとこうなります。Cキーを押しまして…

f:id:wvigler:20210501012752p:plain

再びQキーを押すとこんな感じになります。Healthが80減って20になっていますね。

f:id:wvigler:20210501015003p:plain

敵に近付いてXキーを押して攻撃してみましょう。

f:id:wvigler:20210501011743p:plain

そうしてからまたQキーを押すとこうなります。

f:id:wvigler:20210501015203p:plain

元のHealthは20、そして攻撃力は20なのでその0.4倍である8回復して28になっています。これにてHP吸収攻撃を実装することに成功しました。

 

いやーやっと終わりました。長くなるんじゃないかとは思ってたんですが、めちゃくちゃ長くなってしまいましたね。GameplayEffectはこれでも一部分ではあるんですが…書きながら調べてるとどんどん書くべきことが増えてしまってどうにもならない…

 

さて、お次はGameplayEventとGameplayTask編です。正直ネットワークについて分からないと意味がない部分も多い感じですが頑張ります。それではまた。

 

ご指摘、ご質問、ご意見などあればコメントにお願いします。

 

続きの記事はこちら

wvigler.hatenablog.com

第15回ぷちコン「たぬ吉の大冒険」振り返りその1<AbilitySystemComponent、GameplayAbility、GameplayTag編>

 お疲れさまです。Wviglerです。

今回ヒストリア様主催の第15回ぷちコンに参加させて頂きました。

締切に遅刻したのですが、ありがたいことに発表会で紹介されました。この記事ではその中で得られた知見についての記事です。今回はGameplayAbilitySystemについての知見が多く学べたのでそれについての記事になっています。

そのうち、今回はAbilitySystemComponent、GameplayAbility、GameplayTagについての記事になります。

 

兎にも角にもお品書きを

・AbilitySystemCompoentについて

・とりあえず導入してみる

・GameplayAbilityについて

・GameplayAbilityを導入してみる

・GameplayTagについて

・GameplayTagを導入してみる

 

注》この連なる一連の記事は全てGameplayAbilitySystemについて書かれています。正直英語が読める人なら

github.com

このページか

nerivec.github.io

このページの方が網羅的でオススメです。

 

・AbilitySystemComponentについて

さて全てGameplayホニャララで名付けられているGameplayAbilitySystemはアクションRPGやMOBA、果てはADVやSTGなど、かなり広い範囲のゲームで活用できるシステムです。docs.unrealengine.com

このドキュメント翻訳がアレ過ぎてさっぱり分からんな…

このシステムの利点としてはオフラインゲームではとにかく混乱を抑えられるということが挙げられます。BPの処理を分割してお互いをシンプルにできるので、管理や修正がとてもしやすいです。AIへの導入も割と簡単なのでそれも嬉しいところです。

オンラインだともっと色々利点があるようですが、ここらへんはあまり詳しくないのでわかりません。(勉強しなきゃorz)

現状C++での記述が必須だったりして、まだ少し面倒なところはあるんですが、現状でも十分導入メリットの多いシステムだと感じました。

以前からこの記事この記事などを参考にしながら、実験用のプロジェクト内でGameplayAbilitySystemを扱ったことはあったのですが、GameplayAbilitySystemを自分の作品内で本格導入するのは初となりました。

 

さて、GameplayAbilitySystemは8つの柱でできています。AbilitySystemComponent、GameplayAbility、GameplayTag、GameplayAttribute、GameplayEffect、GameplayTask、GameplayEvent、GameplayCueです。このうち最も根幹となるのが今から紹介するAbilitySystemComponent(以下ASC)です。

とは言ってもASC自身が特に何かをするという訳ではありません。ASCはあくまで他のGameplayAbilitySystemの橋渡し役としてアクセスを提供するだけです。つまりはインターフェイスのようなものですが、実際内部的には"AbilitySystemInterface"というC++インターフェイスを介しています。

・とりあえず導入してみる

注》この記事は基本的にオフラインオンリーの記事となっています。

自分の復習も兼ねて1からGameplayAbilitySystemを導入していきます。

GameplayAbilitySystemの導入にはC++のプロジェクトが必須です。なのでC++コーディングができるプロジェクトから作成していきます。

f:id:wvigler:20210423211152p:plain

TempleteはThirdPersonTempleteを使用します(ホント便利だなこいつ)

f:id:wvigler:20210424024444p:plain

C++プロジェクトにするのを忘れずに…

f:id:wvigler:20210424024545p:plain

プロジェクト名は今回「GAS_Test」としました。

f:id:wvigler:20210424025245p:plain

まずはプラグインの追加をします。

f:id:wvigler:20210424031419p:plain

プラグインをオンにしたら一旦再起動します。

次にエディタを開いて、"GAS_Test.Build.cs"に"GameplayAbilities","GameplayTags","GameplayTasks"のモジュールを追加します。モジュールについてはこちら

f:id:wvigler:20210424035316p:plain

モジュールを追加したら一旦プロジェクトの入っているフォルダを開き、プロジェクトファイルを右クリックしてGenerateVisualStudioProjectFilesをクリックして、プロジェクトを再構成します。(ここ要らないかも…でも検証するの面倒臭い…)f:id:wvigler:20210409091630p:plain

それが終わったら次はいよいよC++クラスの作成です。

f:id:wvigler:20210424025401p:plain

Characterの派生クラスを作成します。

f:id:wvigler:20210424044218p:plain

名前は「GASCharacter」としました。

とりあえずASCを追加していきます。

f:id:wvigler:20210424041412p:plain

GASCharacter.hにAbilitySystemInterface.hをincludeし、(~.generated.hは最下段に無いとコンパイル時エラーになることに注意)

f:id:wvigler:20210424041825p:plain

GASCharacterにIAbilitySystemInterfaceを追加します。

Component実装に使うUAbilitySystemComponent*の変数とAbilitySystemInterfaceに定義されているGetAbilitySystemComponent()を定義します。

f:id:wvigler:20210424043114p:plain

ここは実は…

f:id:wvigler:20210424043404p:plain

このようにUPROPERTYの引数が空でもさほど問題は起きないのかなとは思います。(というか恐らくこっちが正式では?)BP上からASCを直接参照できなくなりますが、その場合GetAbilitySystemComponentを介して呼べば問題ありません。ただいちいちSelfノードとGetAbilitySystemComponentノードを呼ばなくてはならないのはとても面倒くさいので今回はアクセスを可能にしています。

ヘッダはここまでにして、次にGASCharacter.cppファイルを弄っていきます。

まずは"AbilitySystemComponent.h"をincludeします。

f:id:wvigler:20210424044622p:plain

最後にコンストラクタでヘッダで宣言したAbilitySystemをコンポーネントとして登録します。

f:id:wvigler:20210424045540p:plain

以上で実装終了です。コンパイルしましょう。

コンパイルが終わったらBPクラスを作成します。

f:id:wvigler:20210424051029p:plain
名前はBP_GASCharacterとしました。

中身を開くと…

f:id:wvigler:20210424051113p:plain

ちゃんとComponentにAbilitySystemが追加されていますね。

前に述べたとおり、ASCはこれだけで何かをするというものではありません。なのでこのBPクラスは現状ではただのCharacterクラスと何も変わりません。具体的な機能を付けるのはこれからです。

 

…なんですけれども、その前にこのままだと味気ないので、少しBP側で色々やりしょう。

まずは新規にGameModeを作成して、DefaultPawnClassにBP_GASCharacterを登録します。

f:id:wvigler:20210424051951p:plain

そしてこのGameModeをThirdPersonExampleMapのGameModeにOverrideします。

f:id:wvigler:20210424052353p:plain

ThirdPersonExampleMap上のThirdPersonCharacterを削除して、代わりにPlayerStartを設置します。

f:id:wvigler:20210424052908p:plain

今度はBP_GASCharacterを開いて、SpringArmとその子としてCameraのComponentを追加します。設定はデフォルトで大丈夫です。

f:id:wvigler:20210424053200p:plain

次にMeshを追加します。こんな設定にすると…

f:id:wvigler:20210424053427p:plain

こうなります。

f:id:wvigler:20210424053522p:plain

で、今度は移動をさせたいんですが、まともにやろうとするととても面倒臭いので、

f:id:wvigler:20210424054013p:plain

もうこれでいいや(適当)

これでBP_Characterをプレイヤーとして登録して、WASDで最低限の移動ができるようになりました。

・GameplayAbilityについて

 GameplayAbilityはAbilitySystemComponentを除けば、GameplayAbilitySystemの中で最も大きな要素かも知れません。攻撃や防御、回避、ダメージなどあらゆるアニメーションやアクションを排他的に管理可能なシステムです。

実は先に挙げた「たぬ吉の大冒険」では主人公だけでAbilityの数が22個にものぼる、というとんでもない状態になっていました。

f:id:wvigler:20210407155918p:plain

まあAbility作るの慣れてくると本当に楽しいからね。仕方ないね…でも今度から少し自重しようね。あんまり主人公のやれること増やすと調整も大変だしね…

さて、ぷちコンに遅刻した主な理由が判明したところで、プロジェクトにGameplayAbilityを使うための準備をしていきましょう。

・GameplayAbilityを導入してみる

 GameplayAbilityを導入するにはGASCharacter.hを開き、BeginPlay()とPossessedBy(AController* NewControlle)をoverrideする必要があります。

幸いなことにBeginPlay()は作成時に宣言されているので、PossessedByの方を追加で宣言するだけです。

f:id:wvigler:20210424061720p:plain

次に使用するAbilityを登録する配列「AbilityList」を宣言します。

f:id:wvigler:20210424063626p:plain

TSubclassOf<class>はclass以下の派生クラスがUE4エディタ上でツリー形式で選択できるようになります。

f:id:wvigler:20210409082239p:plain

こんな風に表示してくださいね、という意味です。

次にcppファイルに移動して、BeginPlay()とPossessedBy(AController* NewController)を書き換えます。

f:id:wvigler:20210424065208p:plain

このあたりの書き方は何種類かあるみたいなんですが、僕はこの記事に載っている方法を丸写しにしています。

やっていることはAbilityListからAbilityを全て取り出してAbilitySystemにGiveAbility関数を使用して登録する、ということです。

その後でInitAbilityActorInfoでActorInfoを更新します。ActorInfoはGameplayAbility内でActorの情報を取得するために使用するStructureです。後で出てきますが、とても有用な機能なのでこの記述は忘れないようにしましょう。

次はPossessedByです。

f:id:wvigler:20210424065834p:plain

PossessedByはデフォルトでは記述されてないので、新しく書く必要があります。

PossessedByした際にはController情報が切り替わるので、ActorInfoを更新する必要があります。

以上で本当に最低限ですが、C++側の導入は終わりです。コンパイルしましょう。

まぁ殆どのゲームで使う場合GameplayAttributeの実装が必須になったり、Ability自体からTagを弄れた方が便利だから改造しようとか、なんならComponent側からBPでTagを弄れるようにすればもっと…とか欲張りだすと色々キリがないんですが、最低限これでAbilityのみは機能させることはできます。

さてお次はBP側です。

まずはGameplayAbilityのBPを作成します。

f:id:wvigler:20210424072153p:plain

f:id:wvigler:20210424072249p:plain

名前はGPA_Testとしました。

f:id:wvigler:20210424072855p:plain

中を開くと…

f:id:wvigler:20210424073014p:plain

こんな風になっています。右にGameplayTagのリストがありまして、これは非常に重要なものなのですが、それはひとまず置いておいて、とりあえずこれを動くようにしていきます。

f:id:wvigler:20210424073950p:plain

とりあえずAbilityが発動した時に「AbilityStart!!」、Abilityが終わった時に「AbilityEnd!!」と出力だけして終了するものです。CommitAbilityはAbilityのコスト計算などをするものなのですが、とりあえずGameplayAbilityにはおまじない的に頭に付けるものだと思ってもらってもあまり間違いではないです。(例外はありますが…)

これをBP_GASCharacterのAbilityListに登録します。AbilityListはBP_GASCharacterのClassDefaultsからアクセスできます。

f:id:wvigler:20210424080725p:plain

"Z"のインプットノードにこのように繋げます。

f:id:wvigler:20210424081437p:plain

これでプロジェクト実行中にZキーを押すとGPA_Testが発動します。やってみましょう。

f:id:wvigler:20210424081754p:plain

このようになります。ただこの場合まだAbilityが始まった瞬間と終わった瞬間に時間差がないため、「AbilityStart!!」が表示された瞬間に「AbilityEnd!」が表示されています。つまりAbilityが始まった瞬間に終わっているということです。

GameplayAbilityの強みの1つはActivateAbilityからEndAbilityまでの間にLatentなノードを挟めることです。使ってみましょう。

f:id:wvigler:20210424082719p:plain

今度は「AbilityStart!!」が表示された3秒後に「AbilityEnd!!」が表示されるはずです。流石にTimelineは使用できませんが、SetTimerやAnimation系統を扱えます、中でも一番凄いのはPlayMontageが使用可能なことでしょう。

f:id:wvigler:20210409173825p:plain

この2つがヤバイ(語彙がひどい)

またAbilityを所持しているActorの情報を得たいと思ったらGetActorInfoというノードが役に立ちます。これは前にC++内で記述したActorInfoから情報を取得するものです。

f:id:wvigler:20210409174800p:plain

~FromActorInfoというノードはいちいちGetActorInfoからBreakするのが面倒な時のためのものです。BPがスッキリするので、基本的にはできればこっちを使用します。試しにちょっと使ってみましょう。

f:id:wvigler:20210409180118p:plain

こんな風にすると、

f:id:wvigler:20210424084209p:plain

Abilityを所持しているActorであるBP_GASCharacterの名前が表示されます。

文字表示だけだと寂しいので、アニメーションを付けていきましょう。アニメーションはこちらと同じくFrank Action RPG Sword 1 (Basic Set)から取ってきます。

アニメーションリターゲットやスロットアニメーション等に関しては各自で調べていただきまして…

こんな風にノードを組みますと

f:id:wvigler:20210424121854p:plain

Zキーを押すとアニメーションが再生できるようになります。ついでに剣をこちらと同じ方法で作成しました。

f:id:wvigler:20210424122137p:plain

ただしまだ排他処理ができていないので、例えばZキーを連打すると途中でアニメーションがキャンセルされ、また最初からアニメーションが開始されてしまいます。これを解決するために必要なのがGameplayAbilitySystem内の排他処理を担当するGameplayTagです。

・GameplayTagについて

上記のようにすればGameplayAbilityは使用可能になるのですが、正直言ってこれだけではそこまで強力なシステムではありません。GameplayTagによる強力な排他処理が行えれば有用性がぐんと増します。

docs.unrealengine.com

GameplayTag自体はGameplayAbilityとは別個の独立したシステムです。通常ActorやComponentに付いているName型のTagと違い、枝分かれしたツリー状のTagであり、普通のVariableの型として使用することもできます。これだけでも普通に有用なシステムだったりします。

f:id:wvigler:20210409182945p:plain

またGameplayTagContainerという型が存在します。これは名前の通り、複数のGameplayTagをまとめて扱うための型です。

f:id:wvigler:20210409183658p:plain

GameplayAbilityのClassDefaultsにはTagsという項目があり、複数のGameplayTagContainerで排他処理を行う仕組みがあります。

f:id:wvigler:20210409185508p:plain

一つ一つ見ていきましょう

AbilityTagsはAbilityそれ自体が所持するTagです。TryActivateAbilityByTagsでAbilityを指定して実行する場合などに使用します。

CancelAbilitiesWithTagはAbilityがActiveになったと同時にこのTagを所持しているAbilityを全て強制終了(CancelAbility)させます。CancelAbilityはEndAbilityと基本的には同じです。ただし、EventOnEndAbilityノードにはWasCancelledというBooleanPinがあるため、EndAbilityした際とCancelAbilityした際で別々の処理をさせることができます。

f:id:wvigler:20210409191819p:plain

BlockAbilitiesWithTagはAbilityがActiveになっている間はこのTagを所持しているAbilityを実行させません。

CancelAbilitiesWithTagの場合は新たな実行はできますが、それまでActiveになっていたAbilityは強制終了されます。BlockAbilitiesWithTagはすでにActiveになっているAbilityは強制終了はされませんが、新たに実行ができなくなります。

次はActivationOwnedTagsです。ここに設定したGameplayTagはAbilityの実行中、AbilitySystemComponentに付与され、終了すると無くなります。AbilitySystemComponentに付与されているTagとAbility自身の所持するTagは別物ですので注意しましょう。

このAbilitySystemComponentに付与されているTagはBP上で

f:id:wvigler:20210409190618p:plain

これらのノードを使用して調べることができます。ちなみに

f:id:wvigler:20210409190746p:plain

こういうノードもあるんですが、それまでに付与されたTagが全部放り込まれているらしく、活用方法が分かりません。誰か教えて下さい。

さて次です。このAbilityをActiveにしようとした際、ActivationRequiredTagsに指定したTagが全てAbilitySystemに付与されていれば、このAbilityを実行することができます。付与されていなければ実行されません。

ActivationBlockedTagsも同様に指定されたTagが1つでもAbilitySystemに付与されていればこのAbilityは実行されません。付与されていなければ実行されます。

そこから下の項目は実は後で別記事で解説するGameplayEventに関わってきます。なので今は必要ありません。説明はその項目で行います。

・GameplayTagを導入してみる

とりあえず先程のAbilityの問題点を解決しましょう。このAbilityの問題点は「Abilityに排他処理がされていないため、動作中に再びZキーが押されると同じアニメーションが最初から再生されてしまう」というところです。つまりAbilityを実行している間はこのAbilityを再生できないようにしてしまえばいいわけです。なのでこうしてしまいましょう。

f:id:wvigler:20210424125723p:plain

このAbilityが動作している間、Ability.State.Attack01がASCに付与されます。動作中に再びZキーを押しても、ASCにAbility.State.~が付与されているのでAbilityは実行できません。動作が終わるとAbility.State.Attack01はASCから外れるのでAbilityは再び動作するようになります。

さて、GameplayTagはGameplayAbilityの呼び出しにも使用することができます。こちらの方が使い勝手がいいので実装していきましょう。まずAbilityTagsにAbility.Action.Attack01というタグを登録します。

f:id:wvigler:20210424131051p:plain

そしてBP_GASCharacterのTryActivateAbilityByClassノードをTryActivateAbilitiesByTagに入れ替えて、TagContainerにAbility.Action.Attack01を登録します。

f:id:wvigler:20210424132351p:plain

特に何の問題もなく動作することが確認できると思います。そして攻撃モーション中にもう一度Zキーを押しても、再び同じモーションが再生されることはありません。

GameplayTagは攻撃のアニメーション管理にも向いていますが、排他処理が強力なためダメージのアニメーション管理にも向いています。「攻撃中でも敵の攻撃を受けた場合ダメージ処理に移行し、ダメージ処理中は攻撃動作を行えない」などの処理を簡単に実現することができます。

実際に作成してみましょう。

Frank Action RPG Sword 1 (Basic Set)には被ダメージ用のアニメーションも用意されています。

f:id:wvigler:20210424134350p:plain

BP名は「GPA_Damage」としまして

f:id:wvigler:20210424214636p:plain

ノードは…

f:id:wvigler:20210424140855p:plain

こうして、Tagの構成は…

f:id:wvigler:20210424214802p:plain

こうします。要は動作中はAbility.Action.~を持っているAbilityは発動しない設定にします。これを…

f:id:wvigler:20210424215028p:plain

今度はXキーに追加します。実行してみると、Xキーを押すとダメージモーションに移行するのが確認できると思います。それだけでなく例えば攻撃モーション中にXキーを押したり、ダメージモーション中に再びXキーを押してもダメージモーションに移行します。しかしダメージモーション中にZキーを押しても攻撃モーションに移行することはありません。

 

運用上のTipsなのですが、この記事では攻撃もダメージもアニメーションが一つしか無いため問題ではありませんが、AnimationMontageの強みを活かすためにアニメーションはいくつかまとめて管理し、Sectionで実行したほうがいいと思います。f:id:wvigler:20210410044127p:plain

アニメーション同士のLinkを切るのを忘れないように…

そうすれば管理しやすいのに加えて、例えばダメージを受けた際に「いくつかあるダメージモーションからランダムでモーションを選択する」なんて時に…

f:id:wvigler:20210424220557p:plain

こんな風に書けるので…

 

またRootMotionが設定されているアニメーションならあまり問題になりませんが、攻撃時に一瞬足を止めたい場合やジャンプ不可能にしたい場合などは、BPInterfaceで一纏めにして運用することをオススメします。依存度も下げられますしそもそも…

f:id:wvigler:20210410101459p:plain

こんなのいちいち書いてられるか!ってことで。

 

 

とりあえずGameplayAbilityとGameplayTagについての知見はこのぐらいです。

次回はGameplayAttribute&GameplayEffectsにしようかGameplayTaskにしようか…

というかEffectsもTaskもよくよく調べてみるとまだ理解度として低いような…

 

ご意見、ご指摘、ご質問等あればコメントお願いします。

 

続きの記事はこちら

wvigler.hatenablog.com