TPSでの「最前面でなくキャラの奥側に出る照準」*の実装。
コルーチンのウェイト周りに気を付けないと、照準がカクつく要因になる。
*(つまりCanvasのRender ModeがScreen Space – Camera)
サンプル動画
サンプルでは弾のuseGravityを有効化しているので、遠い敵に撃つと着弾地点が下にズレている。
(普通のTPSで使う場合はuseGravityを切れば良い)
照準の画像素材
- ライセンス: CC0
照準の画像を設定
インポート後は普通にSpriteに変更しておく。
- Projectウィンドウ -> 対象の画像を選択。
- インスペクター -> Texture Type: Sprite (2D and UI)に変更。
- Apply
照準の画像を設置
- Canvasを作成。
- インスペクター -> Canvasコンポーネント
- Render Mode: Screen Space – Cameraに変更。
- Render Camera: プレイヤー視点となるメインカメラを紐付けしておく。
- Plane Distance: Sceneウィンドウで横から見て、プレイヤーを少し越える辺りに調整。
- (カメラからプレイヤーへの相対距離+プレイヤーのZ方向のサイズ/2程度)
- Imageを作成。
- インスペクター -> 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;
}
}
使い方
- 射撃タイミングの直前にCheckTargetPositionを呼ぶ。
- 弾を生成。
- 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を有効化したままの透明の画像を設置する。