【Unity】TPSの照準UIを実装

【Unity】TPSの照準周りを実装

TPSでの「最前面でなくキャラの奥側に出る照準」*の実装。
コルーチンのウェイト周りに気を付けないと、照準がカクつく要因になる。
*(つまりCanvasのRender ModeがScreen Space – Camera)

サンプル動画

サンプルでは弾のuseGravityを有効化しているので、遠い敵に撃つと着弾地点が下にズレている。
(普通のTPSで使う場合はuseGravityを切れば良い)

照準の画像素材

シンプル照準画像

  • ライセンス: CC0

照準の画像を設定

インポート後は普通にSpriteに変更しておく。

  1. Projectウィンドウ -> 対象の画像を選択。
  2. インスペクター -> Texture Type: Sprite (2D and UI)に変更。
  3. Apply

照準の画像を設置

  1. Canvasを作成。
  2. インスペクター -> Canvasコンポーネント
    • Render Mode: Screen Space – Cameraに変更。
    • Render Camera: プレイヤー視点となるメインカメラを紐付けしておく。
    • Plane Distance: Sceneウィンドウで横から見て、プレイヤーを少し越える辺りに調整。
    • (カメラからプレイヤーへの相対距離+プレイヤーのZ方向のサイズ/2程度)
  3. Imageを作成。
  4. インスペクター -> Imageコンポーネント
    • Source Image: インポートした照準の画像を指定。
    • Raycast Target: 無効化

コード

GameManager

(GameManagerなのかUIManagerなのか微妙な所だが、一応GameManagerにした)


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class GameManager : MonoBehaviour
{

//作成した照準のImageを紐付けしておく。
	[SerializeField]
	RectTransform reticleRt;

//照準のImageの親キャンバスを紐付けしておく。
	[SerializeField]
	RectTransform reticleCanvasRt;

	Camera mainCamera;


//一応キャッシュしておく。
	PointerEventData pointer = new PointerEventData(EventSystem.current);
	EventSystem eventSystem;

//タッチ位置のUI要素が返されるList。再確保されないようにキャパシティは、やや多めに指定しておく。
	List<RaycastResult> isPointerOverUIResults = new List<RaycastResult>(10);


//1回しか使わないので良いかも知れないが、一応定数化。
	static readonly string EnemyStr = "Enemy";

	Vector3 screenCenterPosition;

	Coroutine aimingSmoothDamp;
	Vector3 touchPosition;
	Vector3 reticleVelocity;

//最終射撃目標位置と現在の射撃目標位置が、この距離以下になるとエイミングを終了する(sqrMagnitude)。
	static readonly float EndAimingSqr = 0.1f;
//【任意の値】射撃目標の敵との判定を取るRayの最大距離。
	static readonly float CheckTouchMaxDistance = 100.0f;

//敵等のエイム時に優先したいオブジェクトのレイヤー。
//SerializeField指定して、インスペクターから設定しても良い。
//コードで複数レイヤーを指定する場合は、「1 << LayerMask.NameToLayer("A") | 1 << LayerMask.NameToLayer("B")」みたいな感じで「|」で区切る。
	LayerMask checkTouchTargetLayer;

	Ray checkTouchRay;

//Rayの判定の結果。一番手前の物だけで良いので1のみ。
	RaycastHit[] checkTouchResults = new RaycastHit[1];

//射撃目標の位置。
	Vector3 targetPosition;

//照準(画像)の位置。
	Vector3 reticlePosition;

//Screen Space - Cameraのキャンバス用に変換した照準の位置。
	Vector2 reticleLocalPosition;

//SmoothDampを完了する大体の長さ(大体なのでピッタリには終わらない)。
	static readonly float AimingSmoothTime = 0.2f;

//SmoothDampする最大速度。多分deltaTimeが掛かるので、想像の100倍程の大きさで設定。
	static readonly float AimingMaxSpeed = 3000.0f;

//【任意の値】カメラから射撃目標位置への相対距離。
	static readonly float CameraToTargetRelativeDistance = 30.0f;

//コルーチンのウェイト。LateUpdate相当。描画系は、このタイミングで更新すれば良い模様。
	static readonly WaitForEndOfFrame EndOfFrameWait = new WaitForEndOfFrame();



	void Awake()
	{


		eventSystem = EventSystem.current;
		checkTouchTargetLayer = 1 << LayerMask.NameToLayer(EnemyStr);
		mainCamera = Camera.main;

//デフォルトの照準位置を計算。
		screenCenterPosition.Set(Screen.width / 2, Screen.height / 2, CameraToTargetRelativeDistance);

//リセット時には、照準位置に適用しておく。
		touchPosition = screenCenterPosition;
		reticlePosition = screenCenterPosition;


	}


	void Update()
	{
		if (Input.GetMouseButton(0)) {
			if (IsPointerOverUI())
				return;

			if (aimingSmoothDamp != null) {
				StopCoroutine(aimingSmoothDamp);
			}
			aimingSmoothDamp = StartCoroutine(AimingSmoothDamp());
		}
	}


	IEnumerator AimingSmoothDamp()
	{
		touchPosition = Input.mousePosition;
		touchPosition.z = CameraToTargetRelativeDistance;

		while (EndAimingSqr < (touchPosition - reticlePosition).sqrMagnitude) {
//連続して実行した時(画面押しっぱなし時)、先にウェイトを入れないとカクつく。
			yield return EndOfFrameWait;

			reticlePosition = Vector3.SmoothDamp(reticlePosition, touchPosition, ref reticleVelocity, AimingSmoothTime, AimingMaxSpeed);
			RectTransformUtility.ScreenPointToLocalPointInRectangle(reticleCanvasRt, reticlePosition, mainCamera, out reticleLocalPosition);

			reticleRt.localPosition = reticleLocalPosition;
		}

		aimingSmoothDamp = null;
	}

//射撃する前にコレを呼んで、照準の位置から射撃目標位置へ変換しておく(照準の先に敵が居る場合はソチラのZ座標を優先する)。
	void CheckTargetPosition()
	{
		checkTouchRay = mainCamera.ScreenPointToRay(reticlePosition);
		if (0 < Physics.RaycastNonAlloc(checkTouchRay, checkTouchResults, CheckTouchMaxDistance, checkTouchTargetLayer)) {
			targetPosition = checkTouchResults[0].point;
		} else {
			targetPosition = mainCamera.ScreenToWorldPoint(reticlePosition);
		}
	}

//タップ(クリック)位置にボタン等のRaycast Targetが有効なUI要素が1つでもあればtrueを返す。
	bool IsPointerOverUI()
	{
		pointer.position = Input.mousePosition;
		eventSystem.RaycastAll(pointer, isPointerOverUIResults);
		return 0 < isPointerOverUIResults.Count;
	}
}

使い方

  1. 射撃タイミングの直前にCheckTargetPositionを呼ぶ。
  2. 弾を生成。
  3. targetPosition目掛けて飛ばす。

コード的にはこんな感じ。
(各コンポーネントをpublic変数に保持している事を想定)


	spawnedBullet.rb.AddForce((targetPosition - (player.tf.position + player.halfHeight)).normalized * spawnedBullet.speed + player.rb.velocity, ForceMode.Impulse);

注意点

  • 弾の出現位置にプレイヤーの座標を使うと、足元から出るので、ちゃんとHalfHeight等を足して調整。
  • 弾とプレイヤーのレイヤー同士の当たり判定は無効化しておく。
  • 弾道にプレイヤーの移動量が乗らないので、AddForce時にプレイヤーのRigidbody.velocityを足しておく(それでも一瞬は荒ぶる)。
  • プレイヤーの移動はFixedUpdateで適用しないと弾がカクつく。

Tips

ボタンを押しそびれた時に照準が移動してしまうのが鬱陶しい

除外領域にしたいボタンの背後に、Raycast Targetを有効化したままの透明の画像を設置する。

タイトルとURLをコピーしました