ミニゲームを作ってUnityを学ぶ![タンクウォーズ編]
###第3回目: 戦車の砲台を制御する
今回は上下の移動ができるようになった戦車について、以下のように砲台を制御する機能を実装していきます。
- 砲台が常にマウスポインタの置かれた位置に向くよう回転させる
#TurretControllerの作成
- TurretControllerという名前のスクリプトを作成
- PlayerTankにTurretControllerをアタッチ
最初に、砲台部分のTransformをインスペクタから設定できるようにします。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TurretController : MonoBehaviour
{
[SerializeField]
[Tooltip("砲台部分のTransform")]
private Transform mTurretTrans;
}
インスペクタに表示されたmTurretTransフィールドには**「PlayerTank/Turret_Base」**をドラッグ&ドロップで設定しておきます。
###Raycastでマウスポインタの現在座標(3D)を取得する
砲台をマウスポインタの位置に向けるためにはまずマウスポインタの座標を取得しなければなりません。
マウスポインタの現在位置はInput.mousePositionで取得することができますが、ここで取得できる値は2D座標のため、3Dであるタンクウォーズでは2Dから3Dへの座標変換が必要になります。
今回はその変換処理をUnityのRaycastで実現します。
Raycast(レイキャスト)とは
ある座標Aを原点としたときに、原点から指定した方向に向けて透明なレーザー光線(Ray)を
真っすぐ飛ばすようなイメージです。
RayはColliderに接触するまで進み、その結果として接触したColliderそのものや接触するまでの
距離を取得することができます。
右の画像は実際のマウスポインタの位置を緑色の丸で示していて、左がカメラの位置(x=0, y=32, z=0)を原点として、カメラから見たマウスポインタの位置に向かってRayを飛ばしている状態を横から見たものです。
左をみるとわかりやすいですが、このときRayがGroundと接触した交点の座標がマウスポインタの位置を3D座標に変換した値となります。
// Ray(光線)を使ってマウスポインタの3D空間上の座標を取得する
public void GetMousePosition3d()
{
// カメラからマウスポインタの方向へ伸びるRayを作成
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Rayが接触したコライダーの情報を格納する入れ物
RaycastHit hit;
// Rayの衝突判定
if(Physics.Raycast(ray, out hit))
{
// 衝突したコライダーのTransformから座標を取得する
Transform hitTrans = hit.transform;
Vector3 position = hitTrans.position;
}
}
上記はRayを使ってマウスポインタの3D空間上の座標を取得するシンプルなコードですが、これはRayがコライダーに接触しなかった場合や、Ground以外の例えば戦車のコライダーに接触した場合の処置が十分ではありません。
それらの想定しうる問題を解消するためにPlaneを利用したコードがこちらです。
// 地面の代わりとして使う仮の平面
private Plane mTempPlane;
// Rayを飛ばした際にmTempPlaneと接触するまでの距離
private float mDistance;
void Awake()
{
// mTempPlaneを予め生成し、Groundと同じ位置に配置しておく
mTempPlane = new Plane();
mTempPlane.SetNormalAndPosition(Vector3.up, Vector3.zero);
}
public void GetMousePosition3d()
{
// カメラからマウスポインタの方向へRayを飛ばす
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (mTempPlane.Raycast(ray, out mDistance))
{
// RayとmTempPlaneとの接点を取得
Vector3 mousePosition3d = ray.GetPoint(mDistance);
}
}
ここで利用しているPlaneはシーン上に配置するオブジェクトとしてのそれではなく、実体のない(目には見えない)平面です。
このPlaneは目に見えないことと同時に、Scaleの概念がなく無限の大きさを持っていることが特徴です。
この特徴によりGroundオブジェクトと同じ場所にPlaneを配置することで、マウスポインタがどの位置にあったとしても必ずPlaneに接触するRayを飛ばすことができます。
また、GetMousePosition3d()で使用しているmTempPlane.RaycastではRayのmTempPlane以外との接触は無視されますので、コライダーが戦車(PlayerTank)なのか地面(Ground)なのかといった判定をする必要がありません。
###砲台の角度を計算する
3D空間でのマウスポインタの座標が取得できましたので、次はその座標に砲台を向けるための角度を計算します。
// 次のFixedUpdateで砲台に適用するRotation値
private Vector3 mTurretRotation = Vector3.zero;
// GetMousePosition3d()から名称を変更
public void CalRotation()
{
// カメラからマウスポインタの方向へRayを飛ばす
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (mTempPlane.Raycast(ray, out mDistance))
{
// RayとmTempPlaneとの接点を取得
Vector3 mousePosition3d = ray.GetPoint(mDistance);
// 対象座標 - 観測座標 = 観測座標から対象座標へ向かうベクトル
Vector3 direction = mousePosition3d - mTurretTrans.position;
// 砲台の正面が対象を向いたときの角度を計算
Quaternion quaternion = Quaternion.FromToRotation(transform.forward, direction);
// 砲台の回転軸をYのみに制限する
mTurretRotation = quaternion.eulerAngles;
mTurretRotation.x = 0.0f;
mTurretRotation.z = 0.0f;
/*
// Y軸の値について、インスペクタ上のRotationでは-180~180の表記だが、コード上では0~360で計算されている
if (mTurretRotation.y < 30.0f)
{
mTurretRotation.y = 30.0f;
}
else if (mTurretRotation.y > 270.0f)
{
mTurretRotation.y = 30.0f;
}
else if (mTurretRotation.y > 150.0f)
{
mTurretRotation.y = 150.0f;
}
*/
}
}
ベクトルの引き算やクォータニオンについての説明は割愛させていただきますが、この修正で砲台の正面がマウスポインタの方向を向いたときの角度が計算できました。
(角度を制限する)
上記コードではQuaternion.FromToRotation()で取得したquternionをVector3に変換して数値の制限を行っていますが、これはquternionがx,y,zの3点軸全てについての回転角度であるのに対して、砲台の回転をY軸のみに制限するためです。
この軸の制限を行っていない場合は砲台が裏返ってしまうような挙動をしてしまいます。
(コメントアウトについて)
コメントアウトされている部分は砲台のY軸回転を時計の短針でいう1時~5時までに制限しています。
これは今回のゲームのルールを「左手に配置されたプレイヤーが右手にいる相手戦車を倒す」にしたときに、左に砲台を回転する必要がないためです。
こちらはPlayerTankを左に配置するタイミングで外してみてください。
###計算した角度を砲台に適用する
では、最終的な角度を実際に砲台のRotationに適用します。
// 計算された角度を砲台のRotationに適用する
public void ApplyRotation()
{
mTurretTrans.rotation = Quaternion.Euler(mTurretRotation);
}
続けて、前回TankMovementで行ったようにTankModelからTurretControllerに対して指示を送るようにコードを修正します。
private TankMovement mMovementScript;
追加 private TurretController mTurretScript;
void Awake()
{
SetLayerCollision();
mMovementScript = GetComponent<TankMovement>();
追加 mTurretScript = GetComponent<TurretController>();
}
void Update()
{
if (IsPlayer && IsActive)
{
// 移動入力の受付
mMovementScript.CheckInput();
// 砲台角度を計算
追加 mTurretScript.CalRotation();
}
}
void FixedUpdate()
{
// 移動力の更新
mMovementScript.ApplyVelocity();
// 砲台角度の更新
追加 mTurretScript.ApplyRotation();
}
最後にプロジェクトを実行して、砲台が常にマウスポインタの位置に向かって回転することを確認します。