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には5、ShotIntervalには0.2、AttackEndTimeには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です。
本来はこういうゲームオーバーのような ゲームの流れに必要な処理はレベルブループリントに書くべきではありません が、今回はお試しでリセットしたかっただけですのでお気になさらず。
さて、ここまででとりあえず ゲームにするための最低限の要素は作ってこれました。 が、正直変数などがややごちゃごちゃしてきて厄介になりかけています。ということで、次回は敵の スポーン周りの修正 を行っていこうと思います。