Unityでキャラクターの移動先を予測して弾を発射する

今回はキャラクターの移動先を予測して弾を発射する機能を作成してみたいと思います。

例えば物理的な弾をキャラクター目掛けて発射したとしてもキャラクターがずっと動いている状態だと前のキャラクターの位置に弾を飛ばすので弾は当たりません。

そこでキャラクターの次の移動先を予測し、弾がキャラクターに当たるようにしてなるべく当たるようにしようという試みです。

今回の機能を作成すると、

↑のような感じでキャラクターの移動先に弾が飛んでくるようになります。

ただ、常にキャラクターの移動先に速い弾が飛んでくるとキャラクターを操作するのにストレスがかかるかもしれません・・・・(^_^;)

状況に応じて変えた方がいいかもしれません。

スポンサーリンク

キャラクターの移動先を予測して弾を飛ばす機能の作成

それでは操作キャラクターの移動先に弾を飛ばす機能を作成していきましょう。

キャラクターの移動先を予測する方法

今回の機能を作成する前にまずはどのようにキャラクターの移動先を予測し弾を当てるようにするかを考えてみます。

キャラクターの次の移動先はキャラクターの現在の位置、現在の向き、現在の移動速度がわかれば次の移動先はキャラクターの位置から現在の向きに現在の速度で移動したと仮定すればわかります。

ただし、その先に弾を発射しても弾がキャラクターに到達するまでには時間がかかります。

その為、キャラクターと弾の発射地点との距離を弾の速さで割って到達時間を計算し、その到達時間でどれだけキャラクターが移動するかも考慮する必要がありそうです。

またキャラクターの基点は足元になっている(3DCGでの設定によりますが)のでそこから少し上を到達点にする為のオフセット値を加味します。

これらの考えを元に計算式を作ると

キャラクターの移動予測値 = キャラクターの現在地 + オフセット値 + (キャラクターの前方 × キャラクターのXZ軸の速度 × 弾の到達時間) + (キャラクタの上方 × キャラクターのY軸の速度 × 弾の到達時間)

となります。

キャラクターの予測移動値の計算ではXZ軸とY軸とを別に計算しますが、これはキャラクターの向いている向きとキャラクターの上方の移動値をそれぞれ計算したい為です。

これでキャラクターの移動予測値の計算式が出来ました。

もっとちゃんとした計算が出来るのかもしれませんが、わたくしには無理のようです。((+_+))

キャラクターの操作スクリプト

キャラクターの移動先を予測する方法がわかったので、次はキャラクターの操作スクリプトを作成します。

キャラクターの設定やスクリプトに関しては

Unityで3Dキャラクターモデルを配置し、キャラクターをCharacterControllerの機能を使って移動させるようなプログラミングをしてみます。

を参照してみてください。

基本的な移動処理以外にキャラクターのXZ軸のCharacterControllerの速度を返すGetVelocityXZメソッドとY軸の速度を返すGetVelocityYメソッド、キャラクターのTransformを返すGetTransformメソッドを作成しています。

CharacterControllerコンポーネントの参照をcharacterControllerに入れていて、そのvelocityでキャラクターの移動速度を取得出来ます。

キャラクターのゲームオブジェクトのインスペクタでTagにPlayerを設定しておきます。

弾のプレハブの作成

次に飛ばす弾のプレハブを作成していきます。

ヒエラルキー上で右クリック→3D Object→Sphereを選択し、名前をBulletとします。

Assetsフォルダ上で右クリックからCreate Materialを選択し、名前をBulletとし弾用のマテリアルを作成します。

Albedoの右のカラーマップで黒色にし、ヒエラルキー上のBulletゲームオブジェクトにドラッグ&ドロップし設定します。

キャラクターの移動値を予測して発射される弾のインスペクタ

今回は弾は物理的に当たらないようにする為Sphere ColliderのIs Triggerにチェックを入れます。

BulletゲームオブジェクトにはPredictionBulletスクリプトを作成し取り付けます。

フィールド宣言部とAwakeメソッド

フィールド宣言とAwakeメソッドの処理を作成します。

コメントを付けてあるので説明はいらないと思います。

OnEnableメソッド

OnEnableメソッドはゲームオブジェクトがアクティブになった時に実行されるので、その時に初期化処理をします。

最初にキャラクターをPlayerタグで探してそこからキャラクター操作スクリプトであるPredictionCharaを取得します。

Vector3.Distanceを使ってキャラクターの位置と弾の現在の位置の距離を計算し、弾の速さで割って到達時間を求めます。

キャラクターの横方向の移動予測先はキャラクターの前方にXZ軸の速度(GetVelocityXZ())の長さ(magnitude)をかけてUpdate1回の移動値を計算し、それに弾の到達時間をかけることで弾が到達するまでのキャラクターの移動値を計算します。

Y軸の移動値はキャラクターの上方にY軸の速度と弾の到達時間をかけて求めます。

キャラクターの予測移動先(predictionCharaPoint)はキャラクターの現在位置に弾が到達するまでの時間でキャラクターが移動値を足したもので計算します。

キャラクターの予測移動先が計算出来たら、弾の現在地からキャラクターの予測移動先までの線を引いて弾の軌跡をシーンビューで確認出来るようにしておきます。

弾の移動は徐々に行う為、次の移動地であるnextPosには現在の弾の位置を設定しておきます。

キャラクターの予測移動先から現在の弾の位置を引いてキャラクターの予測移動先のベクトルを求め.normalizedで正規化して方向を求めます。

弾がずっと存在すると処理速度に影響するのでdeleteTimeで指定した時間が経過したら自動で削除されるようにしておきます。

弾の移動処理と衝突処理

次に弾の移動処理と衝突処理を作成します。

Updateメソッドで

方向 × 速さ × Time.deltaTime

の処理を行い1秒間の移動値を計算しそれをnextPosに足していく事で弾を移動させます。

コメント化してある処理でも同じような処理が出来ますが、Vector3.MoveTowardsを使うと到達点に弾が到達するとその先にいかないのでこの処理はやめました。

Updateメソッドでは移動先を求めているだけで実際には移動させていません。

FixedUpdateメソッドでRigidbodyを使った弾の移動処理を行います。

Updateメソッドで求めた移動先にFixedUpdate内でRigidbodyのMovePositionメソッドを使って移動させます。

BulletゲームオブジェクトのSphere ColliderのIs Triggerにチェックを入れたので、弾は物理的に他のゲームオブジェクトと当たらない為OnTriggerEnterメソッドを使って他のゲームオブジェクトとの衝突を検知します。

Is Triggerのチェックを入れないで物理的に当てる場合はOnCollisionEnterを使ってください。

今回は両方記載しています。

両方ともPlayerタグを設定したキャラクターのコライダと接触した時にコンソールに「ヒット」と表示し、自身のゲームオブジェクトを削除しています。

実際に作成した弾は

弾の実際の大きさ

↑のような大きさになりました。

BulletゲームオブジェクトをAssetsフォルダにドラッグ&ドロップしプレハブにし、ヒエラルキー上のBulletゲームオブジェクトを削除します。

弾をインスタンス化するスクリプトの作成

Bulletプレハブが出来たのでこのプレハブをインスタンス化するスクリプトを作成します。

Main CameraゲームオブジェクトにPredictionShotスクリプトを作成し取り付けます。

指定した時間が経過したらカメラの中心の位置に弾をインスタンス化します。

スクリプトの詳細に関しては

Unityでカメラのレンズの中心からマウスクリックした位置に弾を飛ばす機能を作成していきます。

辺りを参照してください。

このスクリプトでは弾のプレハブをインスタンス化するだけで、弾を実際に移動させる処理はPredictionBulletスクリプトです。

これで機能が完成しました。

弾に重力を働かせたものに対応する

直線で弾を飛ばす場合の対応は出来ましたが重力を働かせた弾の場合はうまくいきません。

そこで、弾に重力を働かせたものにも対応していきます。

ただし、重力に対応したバージョンはなんちゃって数学と物理学で正確な位置には対応していません。

重力対応版を作る為の方法

まずはどのように重力を働かせた弾に対応するかを考えていきます。

今回は砲台(台は作りませんが・・・・)を作り、発射口から弾を飛ばすようにします。

その為、砲台をキャラクターの予測移動先の方に一旦向かせた後、弾が放物線を描いてキャラクターにぶつかるように砲台の筒を上向きに向かせるようにします。

砲台から飛び出した弾がキャラクターの予測移動先で丁度落下するように砲台を上向きにする必要があります。

筒をキャラクターの方向に向かせてから弾が放物線を描いてキャラクターにぶつかるように上向きに回転させますが、角度を計算する為に三角関数を使用します。

砲台の向ける角度を計算

砲台の上向きの角度は三角関数を使って計算すると言いましたが、

三角関数

↑のような直角三角形の場合は

Sinθ = b / c
Cosθ = a / c
Tanθ = b / a

となります。

今回は距離と弾の重力での落下を使って砲台上向きの角度を計算したいのでTanθ = b / aの式を使いたいと思います。

実際に計算で求めたいのはθでアークタンジェントを使うと求めることが出来ます。

θ = Tan⁻¹(b / a)

となります。

Unityではアークタンジェントを計算するメソッドが用意されていますので、a辺とb辺を求めることが出来ればθ(角度)を求めることが出来ます。

辺の長さの求め方

本来は直角三角形を作って計算したいところですが計算が複雑になりそうなので、直角三角形であるとして計算してしまいます。

a辺は「弾が出る位置からキャラクターの予測移動先との距離」
b辺は「弾が重力で落下する距離」

で計算します。

b辺を求める式は

y = v₀t + 1/2 × g × t²

で求めます。

v₀は初期の速さ、tは時間、gは重力加速度です。

弾は上向きに発射するのでv₀(初期の速さ)に時間をかけたものが必要ですが、これは計算出来ないので自由落下のみで計算します。

y = 1/2 × g × t²

tは時間で弾が落下する時間ですが、弾が落下する時間は弾がキャラクターに到達する時間と同じなので、a辺の長さを弾の速さで割って計算した時間を使用するようにします。

砲台を上に向け弾を発射する機能を実装する

砲台の向きを変えて重力が働いた弾をキャラクターの予測移動先に飛ばす機能を実装していきます。

重力を働かせない弾では弾自体に移動する機能を持たせていましたが今回は弾を打った時に力を加えるようにし、弾自体には衝突の処理だけさせるようにします。

弾のプレハブBulletの修正

まずは弾のプレハブであるBulletを選択し、インスペクタのRigidbodyのUse Gravityにチェックを入れます。

これで弾に重力が働くようになりました。

PredictionBulletスクリプトを取り外し、新しくProcessingBulletスクリプトを作り取り付けます。

これで弾自体では衝突判定をするだけになりました。

砲台の作成

次は砲台を作成します。

ヒエラルキー上で右クリックから3D Object→Cubeを選択し名前をButteryとします。

その子要素に空のゲームオブジェクトを作成し名前をBulletHoleとします。

砲台のヒエラルキー

BulletHoleを弾が出る位置に移動させZ軸(青色の軸)の向きが弾が飛んでいく方向になるようにしておきます。

砲台から弾が出る位置にBulletHoleを移動

ButteryにはShotBulletスクリプトを作成し取り付けます。

Batteryのインスペクタ

ShotBulletスクリプトの作成

ShotBulletスクリプトに先ほど考えた処理を書いていきます。

フィールド宣言部

まずはフィールド宣言部分です。

PredictionBulletに書いていたものをShotBulletに移しただけですね。

Start、Updateメソッド

次にStartメソッドとUpdateメソッドを見ていきます。

StartメソッドではBulletHoleゲームオブジェクトのTransformを取得し、キャラクターの操作スクリプトの取得をしています。

Updateメソッドでは指定した時間が経過したらShotメソッドを呼び出して弾を飛ばします。

Shotメソッド

Shotメソッドが実際に弾を飛ばしている処理です。

charaPointは現在のキャラクターの位置+オフセット位置です。

arrivalTimeはキャラクターの位置とBulletHoleの位置の距離をpowerで割って弾の到達時間を計算したものを入れます。

キャラクターの予測移動先の計算は依然と同じです。

キャラクターの予測移動先がわかったら砲台をキャラクターの予測移動先に向けます。

fallingDistanceは重力での落下距離を計算しています。

Mathf.Powを使って累乗を計算していますが、

arrivalTime * arrivalTime

をしているだけです。

2辺からアークタンジェントを計算するのはMathf.Atan2で出来、第1引数にb辺、第2引数にa辺を指定します。

その為、第1引数に落下距離、第2引数に弾を飛ばす位置とキャラクターの位置との距離を指定しています。

これで角度が計算出来たので、transform.Rotateを使ってVector3.right(X軸)を軸としてSpace.Self(ローカル軸)で計算した角度分を上向きに回転させます。

求めたtheta(角度)にMathf.Rad2Degをかけていますが、Mathf.Atan2で求めた角度はラジアンで得られるのでそれを度数に変換する為にMathf.Rad2Deg値をかけています。

弾のプレハブをインスタンス化したら弾に取り付けているRigidbodyコンポーネントを取得し、BulletHoleの前方に力を加えて飛ばします。

フォースモードはForceMode.Impulseを指定します。

ForceMode.Impulseは即座にRigidbodyの質量を考慮して力を加えるモードで力の単位は

質量 × 距離 / 時間

となります。

これで機能が完成しました。

重力を働かせた弾の予測移動先への移動の確認

それでは確認してみましょう。

キャラクターにはスタンダードアセットのFollowTargetスクリプトを取り付けてキャラクターに弾が飛んでいく様子がわかりやすいようにします。

↑のようになりました。

弾の移動速度は決まっているので、キャラクターが遠くに移動すると砲台はどんどん上向きに回転しますが弾は届きません。

キャラクターに弾が届かなくなったら弾を飛ばすのをやめるようにすると処理速度が改善されますね。

ForceMode.Impulseは質量を考慮するので、弾のRigidbodyのmassを大きくすると弾が飛ばなくなります。

おわりに

弾の速さが遅くキャラクターが遠くにいる場合はキャラクターの予測移動先に弾が到達するのが早まったり遅くなったりします。

これは単純にキャラクターの予測移動先の計算が間違っている可能性もありますが・・・・(^_^;)

キャラクターの前回の移動速度を元に弾の到達時間でどれだけ進むかを計算している為、確実にヒットするというわけにはいきません。

また弾が速い場合は確実にキャラクターに当たります。

これはこれで逃げても必ず当たってしまうのでゲームとしては成り立たないですね・・・・。

キャラクターの予測移動先に少しランダムな値を入れて調整してみるのもいいかもしれません。

スポンサーリンク

記事をシェアして頂ける方はこちら

フォローして頂くとやる気が出ます