Technology Blog

技術ブログ

2020.04.24

UE4で1人称視点の3DSTGを作ってみる(第三回)


ではでは今回も進めていきます。

前回では敵のスプライン上での移動方向と移動速度の変化を創っていきましたが、今回は『敵の攻撃』を創っていきます。
いよいよゲームらしくなってきましたね。

ひとまず今回作る敵の攻撃は
『一定の周期で攻撃モードに入る』
『攻撃モード中はプレイヤー方向を向き続ける』
『攻撃モード中は一定の間隔で弾を撃つ』
『弾はまっすぐ飛ぶ』
という仕様で進めていきます。

1.敵の弾を実装する

まずは最低限、敵の弾を実装していきます。
普通にActorを継承して敵弾用のBPを作成します。
名前は『BP_EnemyProjectile』とでもしておきましょう。

BP_EnemyProjectileのコンポーネントを画像のように追加します。
ProjectileCollisionはSphereCollision、ProjectileMeshはStaticMeshにしてください。
ProjectileMovementはその名の通りProjectileMovementComponentです。

それぞれProjectileCollisionは『敵弾の当たり判定』、ProjectileMeshは『敵弾の外見(メッシュ)』を表し、ProjectileMovementComponentはUE4にデフォルトで用意されている弾を飛ばすためのコンポーネントです。

ここからはBP_EnemyProjectileのパラメータをいじっていきます。
まずは『クラスのデフォルト』を選択し、Tagsに敵弾用のタグ…今回は『EnemyProjectile』を追加します。
この部分は少し後の当たり判定実装に必要になってきます。

次に、ProjectileCollisionのSphereRadiusを20、コリジョンプリセットを『Projectile』に設定し、EnableGravityのチェックを外します
SphereRadiusは『ヒットコリジョンの当たり判定』、コリジョンプリセットは『その対象がどの属性に対して当たり判定を実行するか』、EnableGravityは『対象に重力を適用するか』という意味です。

続いて、ProjectileMeshを選択し、StaticMeshに適当なアクタ…今回は『FirstPersonProjectile』を設定し、EnableGravityのチェックを外し、トランスフォームのスケールを{0.1,0.1,0.1}に変更、最後にコリジョンプリセットを『NoCollision』に設定します。
当たり判定自体はProjectileCollisionの方で行っているため、こちらのコリジョンは必要ない、というワケです。

最後にProjectileMovementを選択、飛び道具そのもののパラメータをいじっていきましょう。
ここではInitialSpeedとMaxSpeedをそれぞれ2000に設定しておきます。これはそれぞれ『初期速度』と『最高速』を表します。今回は固定値で進めたいのでどちらも2000にしています。

パラメータをいじるのはここまで。
次はBPですが、ここは画像のようにするだけで大丈夫です。
ここのロジックについては6で説明します

2.敵の射撃処理の実装(1)

さて、敵弾のBPを作成できましたので今度はそれを撃つ処理を実装していきます。
BP_FlyingEnemyを開き、ひとまず以下の変数を追加します。

FirstPersonCharacter…TargetActor…ターゲットのリファレンス
bool…IsAttacking…攻撃モード中かどうか
float…AttackInterval…攻撃を行う周期、編集可能
float…ShotInterval…射撃と射撃の間隔、編集可能
float…AttackEndTime…攻撃を開始してから終了するまでの時間、編集可能
float…ElapsedTime_LastAttack…最後に攻撃してからの経過時間
float…ElapsedTime_AttackStart…攻撃を開始してからの経過時間
float…ElapsedTime_LastShot…最後に弾を撃ってからの経過時間
(型…名前…用途)

『ElapsedTime_〇〇』という似た名前の変数が並んでいてややこしいのですが、これらはそれぞれ値をリセットするタイミングが異なるためこうなっています。LastAttackは『攻撃を終了したタイミング』、AttackStartは『攻撃を開始したタイミング』、LastShotは『弾を撃ったタイミング』でリセットしています。

ひとまずAttackIntervalには5ShotIntervalには0.2AttackEndTimeには5を設定しておきます。

3.敵の射撃処理の実装(2)

次は射撃に関係する関数を実装していきます。
画像のように実装していってください。

まずは射撃を開始する関数StartAttackから。
ここは基本的に各種パラメータのリセットを行っているのみです。
最後のTargetActorのセットは一度だけ実行されるようにしています。GetAllActorsOfClassは『レベル全体から指定したアクタを全て取得する』という便利なノードですが、何度も実行するのは処理負荷的にも避けたいのでこうしています。
(もっというとここはGetAllActorsOfClassを使わないのがベスト)

次は射撃を終了する関数EndAttack、一応StartAttackが関数なのでこちらも関数にした感じですね。
見ての通りIsAttackingを変更しているのみです。

最後に弾をスポーンする関数SpawnProjectileです。
『SpawnActor BP Enemy Projectile』となっているノードはSpawnActorと入力すれば出てくる『クラスからアクタをスポーンする』というノードでClassにBP_EnemyProjectileを設定すれば作れます。
コレは文字通りSpawnTransformの位置/向きに指定されたクラスをスポーンするというノードです。
ただし、SpawnTransformにエネミーのTransformをそのまま設定してしまうとスポーンに失敗してしまいます。
(CollisionHandlingOverrideの設定次第で無理矢理出すこともできますが今回は置いておきます)

ということでGetActorTransformのLocationにGetForwardVectorに100を乗算した値を加算したものを利用します。これは現在のエネミー位置から正面に100だけずらした位置になります。ちなみにGetForwardVectorでは正面方向のベクターを返します

最後にElapsedTime_LastShotの値をリセットするのを忘れずに。

4.敵の射撃処理の実装(3)

ここからはいよいよイベントグラフをいじっていきます。
BP_FlyingEnemyのイベントグラフで触る箇所はTickのみです。

Tickの開始直後にてIsAttackingによる分岐を用意しています。
IsAttackingがfalseの時はSequenceに繋げています。
Sequenceノードは上から順番に処理を実行するマクロです。
SequenceのThen0以降は前回までのTick処理に繋がっており、その部分は今回触りません。今回追加する部分は全てThen1以降です。

まずThen1以降の処理はこうなります。
ElapsedTime_LastAttackのカウントを行い、それがAttackIntervalをオーバーした時にStartAttackを実行しています。
ここでStartAttackが実行されることでTick直後のブランチでFalse以降の処理に入れるということになります。
ちなみに今回のRerouteノードは全てDeltaTimeに繋がっています。

次に開始直後のブランチでFalseになってからの処理です。
ここでは画像のようにします。
SetActorRotationでエネミーの向きを変更したうえでSpawnProjectileを行っています。
FindLookAtRotationのTargetにはGetActorLocationの値をそのまま入れてしまいたいところですが、それではエネミーはプレイヤーの『足元』を見てしまうため、折角撃った弾が当たらなくなってしまう可能性があります。これを避けるため、プレイヤーの中心を狙って打つように調整しています。

そのために必要なのがGetScaledCapsuleHeightです。これはプレイヤーのカプセルの高さ…つまりプレイヤーの高さを表しているため、これをGetActorLocationのZに加算することでプレイヤーの中心座標を拾える、ということになります。

ひとまずこの状態でプレイしてみます。

プレイを開始して5秒後に移動を停止してプレイヤー方向に射撃をしてきたら今のところはOKです。

5.敵の射撃処理の実装(4)

さて、これで敵が弾を撃つようにはなりましたが、まだこれだけではいけません。
この状態では一度攻撃モードに入ってしまうとそのまま止まったままずっと弾を撃ち続けてしまいます
また、弾の量も非常に多く、なかなか気味が悪いことになっています。

ということで次は
『弾を撃ち始めて一定時間が経過すると再度移動を始める』
『射撃と射撃の間にインターバルを入れる』
という処理を創っていきます。

射撃周辺のロジック部分をこの画像のように修正します。
新たなSequenceを用意し、Then0では今までの向き変更処理の後に経過時間の加算処理を入れています。
ここで加算している変数はElapsedTime_LastShotとElapsedTime_AttackStartの2種です。
少し前に触れたように、この二つはリセットのタイミングが違うため個別の変数になっています。

そしてThen1では射撃をするかどうかの分岐を入れています。
射撃ごとにリセットされるElapsedTime_LastShotがShotIntarvalをオーバーするたびに射撃を入れています。
つまり射撃と射撃の間のインターバルですね。

最後のThen2は射撃を終了するかどうかの分岐です。
ElapsedTime_AttackStart、即ち攻撃モードに入ってからの経過時間がAttackEndTimeをオーバーすると射撃終了の処理に入ります。

ここまでの部分を一通り修正したらプレイを開始してみます。
敵が5秒ごとに攻撃モードと移動モードを切り替えるようになり、かつ射撃の頻度が以前よりも少なめになっていれば成功です。

6.敵弾とプレイヤーのヒット判定

ここまでで敵の射撃処理は大体出来上がりました。
ただし、これではまだプレイヤーとのヒット判定が行われていないため、単なる見掛け倒しということになります。
今度はプレイヤー側のヒット判定を創っていきましょう。

FirstPersonCharacterというBPを開きます。コレはFPSテンプレートでプロジェクトを作成すると最初から用意されているプレイヤーのBPです。プレイ中に腕だけ見えてるアレですね。

とりあえず体力を用意し、体力が0になったら死亡するようにしたいため、int型の変数Healthと死亡時用のイベントディスパッチャーOnPlayerHealthZeroを追加します。Healthの値には適当に10とか入れておきます。

そして、イベントグラフにHitイベントを追加し、画像のようにします。
ここでの処理は第2回で敵にダメージを与える処理とほぼ同じですね。
違う個所と言えばActorHasTagでチェックしている対象が『EnemyProjectile』であるという点と、体力が0になったときにOnPlayerHealthZeroを呼び出しているという点のみです。

※補足(敵弾ヒット時のDelayについて)

さて、少し前に作成したBP_EnemyProjectileのヒット処理(画像)についてです。
一見ここのDestroyの直前に入っている0.0秒のDelayは必要なさそうに見えますね?
しかし、ここのDelayを外してしまうと、先ほど用意したHitイベントでのタグチェックでエラーが発生してしまいます。
この原因は簡単で、敵弾側でヒットしたそのフレームでDestroyを行ってしまうとプレイヤー側のヒットイベントではヒットした対象がPendingKill(デストロイ予定)の状態になってしまい、リファレンスを取得できず、結果的にActorHasTagすら使えなくなってしまうためです。
コレを避けるために引数0.0のDelayを用いて1フレームだけ削除を送らせてやる必要があったワケです。

7.死亡時の処理を仮で追加する

さて、実際にプレイして確認したいところですが、現状体力が0になってOnPlayerHealthZeroを呼び出したところで何も起こりません。

そもそもイベントディスパッチャーとは『別のイベントとのバインド(紐づけ)を行い、イベントディスパッチャーを呼び出すと同時にバインドされているイベントも実行する』というものですので、OnPlayerHealthZeroにバインドされているイベントが一切存在しない現状では何も起こらないのは当然です。

ということで現在テストに使用しているレベルのレベルブループリントを開きます。
そして新たなカスタムイベントとしてRestartLevelを追加したうえで、画像のようにBeginPlayからノードを繋げます。
RestartLevel直後のOpenLevelのLevelNameにはテスト用で使っているレベル名を入れてください。

ここは単純にレベル内のプレイヤーのOnPlayerHealthZeroとRestartLevelをバインドさせているだけですね。
これにてOnPlayerHealthZeroが呼ばれると同時にRestartLevelの処理も呼ばれることになります。

つまり、体力が0になってOnPlayerHealthZeroが呼ばれる→レベル側でバインドされたRestartLevelも実行→レベルの再ロードが行われる、という流れで処理が実行されます。

試しにプレイしてみたうえで、敵の攻撃にわざと当たってみます。
Health回数分ヒットするとレベルの最初からやり直しになると思います。うまいことやり直しになれば今回の実装はOKです。

本来はこういうゲームオーバーのようなゲームの流れに必要な処理はレベルブループリントに書くべきではありませんが、今回はお試しでリセットしたかっただけですのでお気になさらず。

さて、ここまででとりあえずゲームにするための最低限の要素は作ってこれました。が、正直変数などがややごちゃごちゃしてきて厄介になりかけています。ということで、次回は敵のスポーン周りの修正を行っていこうと思います。


関連ブログ