2023/6/2~6/3に開催されるUnrealFest 2023 TOKYOに先駆けて、5/22~5/28にかけて「アンリアルクエスト5」が開催されましたので、参加させて頂きました。
アンリアルクエストは、初級者から上級者までが楽しめるイベントで、UnrealEngineでお題を解いていくと最終的にはゲームが完成しているというイベントです。UnrealEngine5の入門用としても使えるイベントですので、興味のあるかたは挑戦してみてはいかがでしょうか?
公式URLは以下の通りです。
https://historia.co.jp/unrealquest05
ちなみに、私が提出した作品は以下のものになります。
でも、安心してください、本解説記事はすごくまじめにクリアされる方向けに書いているので、こんな間違った作品にはならないようになっています。
本記事では、解説が長くなりそうな上級編を後回しにして、一旦、初級・中級編の実装方法について解説していきます。上級編については、1つ1つ別記事としてアップしていく予定ですので、少々おまちください。
1日目クエスト
- 初級:敵を倒せるようにしよう!
- 中級:敵の死亡演出をつけよう!
- 上級:ビヘイビアツリーを使って敵を賢くしよう!
配布されたプロジェクトファイル内では、プレイヤーが攻撃ボタンを押すことで刀を振るようになっているので、刀に当たった瞬間に敵を削除すれば初級クリア、更にエフェクトやアニメーションを付ければ中級クリアとなります。
早速実装していきます。
まずは「初級:敵を倒せるようにしよう!」ですが、BP_UQ5_Playerを開き、刀を確認します。BladeMeshが刀となっているので、これの子コンポーネントとして当たり判定を付けていきます。

BladeMeshを選択した状態で、コンポーネントの追加ボタンをクリックしBoxCollisionを追加します。すると、刀に子コンポーネントとして当たり判定が追加されるので、詳細パネルのトランスフォームの値をコネコネしながら刀に沿うように当たり判定を追加してください。

BoxCollisionのOnComponentBeginOverlapイベントを作成し、敵に当たった場合に敵を倒す処理を書いていきます。

ロジックとしてはこれだけですね。

ただし、これだけだと以下の問題点があります。
- 攻撃中じゃなくても、敵が刀に触れただけでも死亡してしまう
- プレイヤーの刀と敵の刀が触れただけでも死亡してしまう
それを解消したロジックが以下のものになります。OtherActorが敵であることを確認したうえで、OtherCompが敵のカプセルコンポーネントか否かの判定をいれており、刀が敵の体を斬りつけたか否かを判定しています。さらに、IsAttackという変数が用意されていたので、これをAND演算することで「プレイヤーの攻撃中に刀が敵の体に触れたら、敵は死亡する」というロジックになっています。

次に中級となる死亡演出をつけていきます。初級のままだと、敵キャラクタをDestroyActorしているだけなので切りつけた瞬間に消えてしまいます。
本来、敵の死亡演出はプレイヤーが司るものではなく、敵が敵自身で死亡すべきなので、敵のブループリントを開き死亡時の処理を書いていきます。
BP_UQ5_Enemyを開き、カスタムイベントを追加してDeadという名前にします。
一旦、DeadからはDestroyActorを呼び出すだけにしておき、プレイヤーの刀が当たった処理をDestroyActorから敵BPのDeadを呼び出すように修正します。


プレイしてみると今までと何も変わらない事が分かりますが、敵の死亡演出を敵のBPで実装できるという自然な形になったかと思います。
最後に敵を倒した時にダメージを受けたアニメーションを入れて、更にラグドール化して死んだ感じにしてみましょう。
コンテンツドロワーからAS_Damaged(アニメーションシーケンス)を探し、アニメーションモンタージュを作成して名前をAM_Damagedにします。
そして敵のBPのDead処理を以下のように修正していきます。

これで1日目の初級・中級はクリアです。
それっぽくなりましたね。
2日目クエスト
- 初級:敵を全滅させたら勝ち
- 中級:敵が自動的に出てくる仕組みを作ろう!/敵にHPをつけよう
- 上級:敵の攻撃を弾く仕組みを作ろう
まずは初級から手をつけたいところですが、中級の「敵が自動的に出てくる仕組みを作ろう」と同時進行で作っていきたいと思います。理由は、敵が出現や全滅を判定する為の処理は1つにまとめられそうな為です。
ということで、早速ですが敵の出現・全滅を管理するBPを作ります。Actorを親クラスとして、BP_EnemyManagerを作成します。

作成したEnemyManagerにBoxCollisionコンポーネントを配置し、適当な大きさに広げます。

さらに、BP_EnemyManagerのTickInterval(secs)を10に設定します。今回はスポーン用のタイマーを用意するのが面倒なので、Tickの間隔を10秒にして、Tick処理で敵を出現させようという目論見です。

BPのロジックは以下のように組んでいきます。SpawnEnemyというカスタムイベントを作成し、BoxCollisionの範囲からランダムで敵をスポーンさせてAIを有効にさせているだけです。TickからSpawnEnemyを呼び出すことで、10秒に1体ずつ敵が登場します。

最後にレベル上の適当な位置にBP_EnemyManagerを設置します。

これでプレイしてみると、10秒に1体ずつ敵が出現するはずです。
次はBP_EnemyManagerに敵の全滅判定を入れていきます。常にレベル上の敵の数を監視しても良いのですが、処理負荷が高いのと自律的ではない感じがするので、敵がスポーンされた時に敵のカウントしていき、敵が倒された時に敵のカウントをマイナス1していく処理にしてみます。
敵が倒されたという事は、敵自身が「私は死にました」とBP_EnemyMangareに伝える必要があるので、敵キャラクターが生まれた瞬間にBP_EnemyManagerの情報を教えてあげるようにしましょう。
敵のBPを開いてManagerという変数を追加します。型はもちろんBP_EnemyManagerです。更に変数定義の右側にある目のアイコンをクリックし、詳細パネルにある「スポーン時に公開」にもチェックを入れてコンパイルして保存します。

BP_EnemyManagerに戻ると、敵をスポーンしているノードにManagerというピンが追加されています。(コンパイルや保存状態によって表示されていない場合がありますので、全て保存したりBPを開きなおしたりしてみると表示されるようになります)
ここに、BP_EnemyManager自身を入れてあげればいいので、Selfノードを繋げます。

更にBP_EnemyManagerには現在の敵の数を管理する仕組みを入れます。NowEnemyCountというInteger型の変数を用意し、カスタムイベントでAddEnemyCountを作ります。AddEnemyCountを通じて敵キャラクターの増減を管理し、カウンタが0になったら全滅と表示させる処理とします。

次に敵をスポーンした時に現在の敵のキャラクター数を+1するので、スポーン後の処理にAddEnemyCountを呼び出してAddCountに1を渡します。

最後に、敵が死んだ時にカウントからマイナス1する処理を入れます。これはBP_UQ5_EnemyのDead処理に追加します。ここで重要なのは、スポーン時に渡されたManager(BP_EnemyManager)のAddEnemyCountを呼び出しているところです。敵キャラクターをスポーンしたのは誰か?という情報を生まれた瞬間から持っており、死ぬときはそこに「私は死にました」と教えているのです。
また、死亡処理が連続して実行されないようにDoOnceを入れておきました。

これで、画面上から敵が一掃された時に「全滅」と表示されるようになりました。
最後に敵にHPを付けていきます。BP_UQ5_EnemyにHPというFloat形の変数を作成します。一度コンパイルし、HPのデフォルト値に3.0と入力してください。更に、AnyDamageイベントを追加し、HPを削っていく処理と死亡判定処理を入れます。ロジックは以下の通りです。(連続してダメージを受けないようにIsDamageというBool型の変数も追加している点と、Deadイベントにあったダメージを受けた際のアニメーションモンタージュの再生が、AnyDamage側に移動してきていますので注意してください。)

最後にプレイヤー(BP_UQ5_Player)の刀の当たり判定処理で呼んでいたDeadをAnyDamageに変更します。ダメージは常に1.0という事にしておき、3回攻撃が当たったら敵が死ぬ事にしました。

これで、3回攻撃を当てたら死ぬ敵の完成です。
3日目クエスト
- 初級:前転する仕組みを作ろう/プレイヤーのHPを削られたら負け
- 中級:連続攻撃をしよう
- 上級:必殺技を入れよう
まずは前転を実装していきます。今回は丁寧に前転のアニメーションが入っているのでそれをそのまま使わせてもらいます。ただ、その前に、、、前転する時のボタンを定義する必要がありますね。
ということで、EnhancedInputのボタン定義の追加から行っていきます。
既に移動・視点移動・ジャンプ・アタック等の定義があるので、そこに新規で入力アクションを作成し、名前は”IA_Rolling”としました。単純なボタンイベントになるので、これは作るだけで終わりです。

次に入力マッピングコンテキストである”IMC_Default”を開きます。マッピングの+ボタンをクリックし、表示されたドロップダウンで先ほど作成したIA_Rollingを選択します。今回はRollingということで、キーボードのRキーを押下した時に前転するようにしますが、キーは何でも構いません。

次に前転のアニメーションモンタージュを作成します。提供されているアニメーションの中から”AS_Rolling”を右クリックし、”作成する”からAnimationMontageを作成し、名前を”AM_Rolling”としました。

プレイヤーのブループリント(BP_UQ5_Player)にて、Rボタン押下時に前転させるアニメーションの実行を作っていきます。

プレイしてみると・・・
不思議な動きをしていますね・・・これはアニメーション的にはキャラクターは移動している状態だけど、実際のキャラクターは移動していない為に発生しています。これを解決するには”AS_Rolling”を開いて、アセット詳細のEnableRootMotionにチェックを入れて保存するだけです。

これで前転しても戻ってくることは無くなりました。
「プレイヤーのHPが削られたら負け」については、既に敵キャラクターのHPを実装しているので詳細の解説は省きますので、実装のBPのみ掲載します。
プレイヤーBPは、DeadイベントとAnyDamageを敵キャラクターからコピーします。ただし、BP_EnemyManagerに死んだことを伝える必要が無い点が異なります。また、プレイヤー死亡後はキー入力を無効化する処理が追加されています。

敵キャラクターのBPは、攻撃処理でIsAttackを使いたかったのでプレイヤー側から攻撃処理の一部をコピーします。

更に、敵キャラクターの刀にもBoxCollisionを追加して、プレイヤーが当たった場合にダメージを与えるようにします。(単純にキャストする型が違うだけですね)

これでプレイヤーも3回切り付けられたら死亡するようになりました。
次に連続攻撃を実装していきます。
UnrealEngineで連続攻撃を調べるとGAS(GameAbilitySystem)が引っ掛かることが多いのですが、今回はアンリアルクエスト5の打ち上げDiscord内でEpicGamesの岡田さんが披露していたやり方で実装してみたいと思います。(実装の概念しか見ていなかったので、岡田さんの実装とは異なる部分もあるかと思いますので、ご了承ください)
まずは、BP_UQ5_Playerに連続攻撃用の変数を3つ追加します。1つ目は連続攻撃アニメーションを保持する為のAttackAnimsでAnimMontage型の配列として定義します。2つ目は連続攻撃の何番目を実行中か判断する為のAttackIndexというInteger型を定義します。3つ目は連続攻撃のボタン受付を開始したか否かを判断する為のIsNextAttackStandbyをBoolean型で定義します。
コンパイルし、AttackAnimsに、攻撃1・攻撃2・攻撃3のアニメーションモンタージュ(攻撃2(AM_Attack2)と攻撃3(AM_Attack3)は新規作成してください)を格納しておきます。つまり、以下のBPのようにAttackAnimsの配列に従ってアニメーションを実行することで、連続攻撃を実現しています。(AttackIndexが0のままなら、今までと全く同じ動きとなる)

次にアニメーションモンタージュまでを実装していきます。
AttackIndexが0の場合は攻撃1、それ以上の場合は連続攻撃中で攻撃2 or 攻撃3という判断ができます。つまり、この値が0の場合は今までと一緒の条件でアニメーションモンタージュを再生すれば良く、それ以外の場合は、IsNextAttackStandbyがtrueになっている場合のみ次の攻撃アニメーションを再生します。
また、モンタージュ再生前にIsNextAttackStandby変数をfalseに設定することで次の攻撃を許可するか否かのフラグを一旦折っておきます。

モンタージュ再生後は以下のようになります。
OnNotifyBeginイベントは、攻撃アニメーションの「あるコマ」に達した時にコールされるイベントとなります。攻撃アニメーションが次の攻撃へ繋げられる状態になった時に呼び出されるように設定しておけばこのピンが実行されます。すると、連続攻撃の入力を受け付けるフラグIsNextAttackStandbyがtrueになり、攻撃アニメーションインデックス(AttackIndex)に1が加算されて、次の攻撃アニメーションの実行に備えます。
また、OnBlendOutのピンの処理にはAttackIndexを0に初期化する処理が追加されています。これは連続攻撃のボタン入力が無かった場合に連続攻撃を終了して、改めて1から攻撃する処理となります。

ロジックとしてはこれで完成ですが、先ほど説明した通りOnNotifyBeginを呼び出す処理をアニメーション側に追加しなくてはいけません。AM_Attack1とAM_Attack2の通知欄で右クリックして「通知を追加」から「Montage Notify」を追加してください。(AM_Attakc1は2への連続攻撃を許可するタイミング、AM_Attack2は3への連続攻撃を許可するタイミングとなります)

本来はMontage Notifyが複数存在することを考慮しなければなりませんが、今回は1つしかありませんので、NotifyNameによる判断処理は行っていません。
これで連続攻撃の実装が完了しました。
4日目クエスト
- 初級:ステージを作って飾ろう/回復アイテムをいれよう
- 中級:爆弾を作ろう
- 上級:敵のバリエーションを作ろう
ステージについては、様々なアセットを並べて頂ければと思います。これによって世界観が変わってくるので、結構重要なクエストですが、ここでは割愛させて頂きます。
ということで、まずは回復アイテムを作成します。
回復アイテムのActorブループリントを作成し、プレイヤーがそれに触れた場合にHPを回復させてみましょう。ということで、まずは新規ブループリント作成で、ActorをベースにしてBP_Potionを作成します。

今回のアンリアルクエストは丁寧に回復薬のMeshまで用意してくれているので、それを使います。BP_PotionにStaticMeshコンポーネントを追加して回復薬のMeshを選択します。回復薬は衝突判定が不要なのでコリジョンプリセットを”BlockAllDynamic”から”OverlapAll”に変更します。
次にBeginOverlapイベントを作成します。プレイヤーが回復薬を取るという処理は、敵の刀がプレイヤーに当たるという処理とほぼ同じ実装となり、異なる点はダメージを与えるか、ダメージを回復するかだけです。(細かい点を言えば、アイテムは取得後に消えてしまう必要があり、IsAttackフラグを使う必要がないという点が異なります)

裏技的な使いかたですが、ApplyDamageにマイナスを渡すことでプレイヤーのHPを回復させています。
プレイヤーのHPが表示されておらず回復しているか分かりづらい為、一旦PrintStringノードでHPを表示してみます。BP_UQ5_PlayerのAnyDamage処理内で現在のHPを表示してみましょう。

これでレベル上にBP_Potionを適当に配置してプレイしてみると、回復薬を取るとHPが1ずつ回復していることが分かります。
引き続き爆弾を作ってみます。新規ブループリントでActorをベースとしたBP_Bombを作成し、StaticMeshコンポーネントを追加して爆弾のMeshを設定します。(ここまでの手順は回復薬と全く同じです)
爆弾は一定時間経過後に爆発して敵にダメージを与える(プレイヤーはダメージを受けないという事にしましょう)ので、その処理を実装していきます。
まずは爆弾を投げる為のボタンを用意します。これは前転の時と同じなので説明は割愛しますが、今回は”B”ボタンで爆弾を投げるようにしました。


次に爆発エフェクトを作ります。これはNiagaraで作ってみましょう。
新規でナイアガラシステムを作成し、「選択したエミッタに基づく新しいシステム」を選択して”次へ”をクリックし、「Fountain」を追加して終了ボタンをクリックしてください。名前は”NS_Bomb”とします。
Niagaraの説明をすると長くなってしまうので、すごく簡単に爆発っぽいエフェクトを作ってみましょう。Niagaraの設定項目を以下のようにしてみてください。
- SpawnRateのSpawnRateを5000に
- Emitter StateのLoobBehaviorを”Once”に
- InitializeParticleのLifetimeMinを0.3に、LifetimeMaxを0.5に
- InitializeParticleのColorModeのColorを赤っぽく
- AddVelocityのVelocityModeを”FromPoint”に
- パーティクル更新のGravityForceとDragを削除
すごく雑ですが、これだけで爆発っぽいエフェクトになります。
先ほど定義したBボタンの実装を行います。IA_Bombイベントにてプレイヤーの向いている方向に対してBP_Bombをスポーンし、AddImpulseノードで爆弾を前方に投げるようにしてみました。

最後に爆弾の実装です。BP_Bombを開き、爆弾を物理で動かすために爆弾のStaticMeshコンポーネントを選択した状態で”SimulatePhysicsにチェックを入れて、コリジョンプリセットを”PhysicsActorに変更します。
更にBignPlayから3秒後に爆発のエリア(StaticMeshの子コンポーネントとしてSphereコリジョンを生成しサイズを0にしておく)を拡大して、爆発エフェクトを表示して爆弾が消滅するロジックとしました。また、

爆発の範囲に敵が入ると、ApplyDamageで3.0を敵に与える為、HP3.0の敵は即死するという処理になっています。
完成版はこちらになります。
5日目クエスト
- 初級:SE・BGMをつけよう/エフェクトを入れよう
- 中級:UIを実装しよう
- 上級:こだわりのヒットストップを作ろう
初級のエフェクトについては4日目のクエストで爆弾の爆発エフェクトを作成したので割愛します。
SE・BGMについてもPlaySound2D・PlaySoundLocationノード再生したり、サウンドキューをレベルに配置するだけですので、フリーの効果音等をインポートして再生してみてください。
ということで5日目はUIについて作っていこうと思います。
UIはウィジェットブループリントを使用して作成します。新規に作成し名前を”WBP_Main”としました。
エディタを開くとこんな感じになります。

パレットからCanvasを選択しドラッグしてエディットエリアに配置すると、画面の大枠が表示されます。今回はプレイヤーのHPのみをUIとして表示しようと思いますので、テキストブロックを画面上部中央に配置しました。

グラフに切り替えて、UI上のHPを更新するカスタムイベントを作成します。名前は”UpdateHP”として、ロジックは以下のようにします。

BP_UQ5_PlayerにUIの表示処理を追加します。変数にWBPMainという名前でWBP_Main型の変数を作っておきBeginPlayの最後に以下のロジックを追加します。これは、WBP_Mainを生成し、それを変数に保持したあとに画面に表示しているロジックとなります。

更にカスタムイベント”UpdateHPforUI”を作成し、プレイヤーのHPをUIに伝える処理を記述します。

作成した”UpdateJPforUI”をBeginPlayの最後と、AnyManage処理でHPの更新後に呼び出すようにします。


これでUIとしてプレイヤーのHPが画面に表示されるようになりました。早速プレイしてみます。
これでUIの実装は完了です。
駆け足でしたが、各日のクエストの初級・中級のクリア方法について解説させて頂きました。
追って上級のクリア方法についても記事にしていきたいと思っていますので、少々お待ちいただければと思います。
それでは楽しいアンリアルライフを!!
No responses yet