Unityを触り始めた頃、「カメラのズーム処理も、移動も、タップ判定も、全部ひとつの Update() に書いちゃえ!」となりがちですよね。
短い間は動いてくれますが、時間が経つと
- どの処理がどの入力に対応しているのか分からない
- カメラ専用の調整をしたいだけなのに、プレイヤーコードまで巻き込んでバグる
- スマホ用のピンチズームを追加したら、マウスホイールのズームとごちゃごちゃになる
といった「巨大なGodクラス」の沼にハマりがちです。
そこでこの記事では、「カメラのピンチズームだけ」を担当する小さなコンポーネントとして
PinchZoom コンポーネントを用意して、スマホの2本指操作でカメラのズームをきれいに分離して実装していきます。
【Unity】スマホで直感ズーム!「PinchZoom」コンポーネント
このコンポーネントは、
- スマホの2本指ピンチ操作でズームイン / ズームアウト
- 透視カメラ(Perspective)では
fieldOfViewを変更 - 正投影カメラ(Orthographic)では
orthographicSizeを変更 - 最小・最大ズーム量をインスペクターから調整可能
という役割だけに責務を絞っています。
プレイヤー移動やスワイプ回転などは別コンポーネントに分けることで、コードがスッキリして保守しやすくなります。
フルコード:PinchZoom.cs
using UnityEngine;
/// <summary>
/// スマホの2本指ピンチ操作でカメラのズームを行うコンポーネント。
/// ・透視カメラ: fieldOfView を変更
/// ・正投影カメラ: orthographicSize を変更
/// カメラにアタッチして使います。
/// </summary>
[RequireComponent(typeof(Camera))]
public class PinchZoom : MonoBehaviour
{
[Header("ズーム設定")]
[SerializeField]
[Tooltip("ピンチ操作の感度。大きいほど少しの指の移動で大きくズームします。")]
private float zoomSensitivity = 0.1f;
[SerializeField]
[Tooltip("透視カメラ(Perspective)のときの最小FOV。数値が小さいほどズームインします。")]
private float minFieldOfView = 20f;
[SerializeField]
[Tooltip("透視カメラ(Perspective)のときの最大FOV。数値が大きいほどズームアウトします。")]
private float maxFieldOfView = 60f;
[SerializeField]
[Tooltip("正投影カメラ(Orthographic)のときの最小サイズ。小さいほどズームインします。")]
private float minOrthographicSize = 3f;
[SerializeField]
[Tooltip("正投影カメラ(Orthographic)のときの最大サイズ。大きいほどズームアウトします。")]
private float maxOrthographicSize = 15f;
[Header("プラットフォーム設定")]
[SerializeField]
[Tooltip("エディタやPC実行時に、マウスホイールでズームをテストできるようにするか")]
private bool enableMouseWheelInEditor = true;
[SerializeField]
[Tooltip("マウスホイールズームの感度")]
private float mouseWheelSensitivity = 5f;
// このコンポーネントが参照するカメラ
private Camera targetCamera;
private void Awake()
{
// 同じGameObject上のCameraコンポーネントを取得
targetCamera = GetComponent<Camera>();
// 最小値・最大値の関係が逆になっていたら、エディタ上で気付きやすいように補正
if (minFieldOfView > maxFieldOfView)
{
float tmp = minFieldOfView;
minFieldOfView = maxFieldOfView;
maxFieldOfView = tmp;
}
if (minOrthographicSize > maxOrthographicSize)
{
float tmp = minOrthographicSize;
minOrthographicSize = maxOrthographicSize;
maxOrthographicSize = tmp;
}
}
private void Update()
{
// スマホ用のピンチズームを優先して処理
HandleTouchPinchZoom();
// エディタやPC実行時のテスト用に、マウスホイールズームもサポート
HandleMouseWheelZoomInEditor();
}
/// <summary>
/// 2本指のピンチ操作を検出して、カメラのズームを行う処理。
/// 実機スマホでの操作を想定しています。
/// </summary>
private void HandleTouchPinchZoom()
{
// タッチが2本でない場合はピンチ処理を行わない
if (Input.touchCount != 2)
{
return;
}
// 2本の指を取得
Touch touch0 = Input.GetTouch(0);
Touch touch1 = Input.GetTouch(1);
// 各指の「前フレームの位置」を計算
Vector2 touch0PrevPos = touch0.position - touch0.deltaPosition;
Vector2 touch1PrevPos = touch1.position - touch1.deltaPosition;
// 前フレームと今フレームの「2本指の距離」を算出
float prevMagnitude = (touch0PrevPos - touch1PrevPos).magnitude;
float currentMagnitude = (touch0.position - touch1.position).magnitude;
// 距離の差分(正: 指が広がった = ズームアウト / 負: 指が近づいた = ズームイン)
float difference = currentMagnitude - prevMagnitude;
// 差分を感度でスケーリングしてズーム処理へ
ApplyZoom(-difference * zoomSensitivity);
// ※マイナスを掛けているのは「指が広がるほどズームアウト」の直感に合わせるため
}
/// <summary>
/// エディタやPC実行時に、マウスホイールでズームをテストするための処理。
/// スマホビルド時には無視されます(enableMouseWheelInEditor が false の場合)。
/// </summary>
private void HandleMouseWheelZoomInEditor()
{
#if UNITY_EDITOR || UNITY_STANDALONE
if (!enableMouseWheelInEditor)
{
return;
}
// マウスホイールの回転量を取得(上スクロールで正の値、下で負の値)
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Approximately(scroll, 0f))
{
return;
}
// スクロール量を感度でスケーリングしてズーム処理へ
// (PCでは「上スクロール = ズームイン」となるようにマイナスを掛ける)
ApplyZoom(-scroll * mouseWheelSensitivity);
#endif
}
/// <summary>
/// 実際にカメラのパラメータを変更してズームを適用する共通処理。
/// 透視カメラと正投影カメラで挙動を切り替えます。
/// </summary>
/// <param name="zoomDelta">ズームの変化量。正でズームイン、負でズームアウト。</param>
private void ApplyZoom(float zoomDelta)
{
if (targetCamera == null)
{
return;
}
if (!targetCamera.orthographic)
{
// 透視カメラの場合は fieldOfView を変更
float fov = targetCamera.fieldOfView;
// ズーム量を反映
fov -= zoomDelta;
// 最小値・最大値でクランプ
fov = Mathf.Clamp(fov, minFieldOfView, maxFieldOfView);
targetCamera.fieldOfView = fov;
}
else
{
// 正投影カメラの場合は orthographicSize を変更
float size = targetCamera.orthographicSize;
// ズーム量を反映
size -= zoomDelta;
// 最小値・最大値でクランプ
size = Mathf.Clamp(size, minOrthographicSize, maxOrthographicSize);
targetCamera.orthographicSize = size;
}
}
/// <summary>
/// 現在のズーム量を 0~1 の範囲で取得するヘルパー。
/// UIスライダーなどと連携したいときに便利です。
/// </summary>
/// <returns>0 = 最小ズーム(引き)、1 = 最大ズーム(寄り)</returns>
public float GetNormalizedZoom()
{
if (!targetCamera.orthographic)
{
// 透視カメラの場合、FOVが小さいほど「寄り」なので逆転させる
float t = Mathf.InverseLerp(maxFieldOfView, minFieldOfView, targetCamera.fieldOfView);
return t;
}
else
{
// 正投影カメラの場合、サイズが小さいほど「寄り」なので逆転させる
float t = Mathf.InverseLerp(maxOrthographicSize, minOrthographicSize, targetCamera.orthographicSize);
return t;
}
}
}
使い方の手順
ここでは「スマホ向けの見下ろし型アクションゲーム」のカメラにピンチズームを付ける例で説明します。
-
カメラにコンポーネントをアタッチする
- ヒエラルキーで
Main Camera(またはズームさせたいカメラ)を選択 - Inspector の「Add Component」ボタンから
PinchZoomを追加 [RequireComponent(typeof(Camera))]が付いているので、Camera が無い場合は自動で追加されます
- ヒエラルキーで
-
ズームの範囲と感度を調整する
- 透視カメラの場合:
Min Field Of View:寄りの限界(例: 20)Max Field Of View:引きの限界(例: 60)
- 正投影カメラの場合:
Min Orthographic Size:寄りの限界(例: 3)Max Orthographic Size:引きの限界(例: 15)
Zoom Sensitivityでピンチの感度を調整(0.05~0.2 くらいから試すと良いです)
- 透視カメラの場合:
-
エディタでの動作確認(マウスホイール)
- PCやエディタ上でテストしやすいように、
Enable Mouse Wheel In Editorをオンにしておきます - ゲーム再生中にマウスホイールを上下するとズームイン / ズームアウトします
- スマホビルド時は、実機で2本指ピンチを使ってズームを確認します
- PCやエディタ上でテストしやすいように、
-
具体的な使用例
いくつかのシーンでの使い方を挙げてみます。-
プレイヤー追従カメラ + ピンチズーム
別コンポーネントで「プレイヤーを追いかける処理」だけを書いたFollowTargetをカメラに付けておき、
そこにPinchZoomを追加するだけで、「追従しつつ、プレイヤーが自由にズームできるカメラ」が完成します。
追従のロジックとズームのロジックが分離されているので、どちらか片方だけ差し替えるのも簡単です。 -
RTSやシミュレーションの俯瞰カメラ
正投影カメラを使ってマップ全体を上から見下ろすタイプのゲームでは、
Orthographic Sizeの最小・最大を適切に設定することで、「ユニットを細かく見たいときは寄る」「戦況全体を見たいときは引く」といった操作感を簡単に実現できます。 -
動く床の確認用カメラ
ギミックが多い3Dステージでは、レベルデザインの確認用に「自由にズームできるデバッグカメラ」を用意しておくと便利です。
シーンにデバッグ用カメラを1つ置き、PinchZoomを付けておけば、動く床やトラップの動きを実機で細かくチェックしやすくなります。
-
プレイヤー追従カメラ + ピンチズーム
メリットと応用
PinchZoom をひとつのコンポーネントとして分離することで、
- 「カメラのズーム処理」を他の入力・カメラロジックから切り離せる
- プレイヤー用カメラ、敵視点カメラ、リプレイカメラなど、どのカメラにも同じズーム挙動を簡単に再利用できる
- プレハブ化しておけば、「ズーム付きカメラ」のプレハブをシーンにポンと置くだけで使い回せる
- レベルデザイン時に「このシーンのカメラだけズーム範囲を変えたい」といった要望にも、インスペクターの数値調整だけで対応できる
といったメリットがあります。
「ズームは PinchZoom に任せて、カメラの向きや追従は別コンポーネントで」と責務を分けることで、
大きなGodクラスを避けながら、カメラ周りの機能を少しずつ積み上げていけます。
最後に、簡単な「改造案」として、ズーム量に応じて背景のUIの透明度を変える例を示します。
これも別コンポーネントとして分離しておくと、UI側の調整が楽になります。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// PinchZoom のズーム量に応じて、UIイメージの透明度を変化させるコンポーネント。
/// ズームインするとUIが薄くなり、ズームアウトすると濃くなる例。
/// </summary>
[RequireComponent(typeof(Image))]
public class ZoomBasedUIFader : MonoBehaviour
{
[SerializeField]
[Tooltip("ズーム情報を取得する PinchZoom コンポーネント")]
private PinchZoom pinchZoom;
[SerializeField]
[Tooltip("ズームが最小(引き)のときのアルファ値")]
private float alphaAtMinZoom = 1f;
[SerializeField]
[Tooltip("ズームが最大(寄り)のときのアルファ値")]
private float alphaAtMaxZoom = 0.2f;
private Image image;
private void Awake()
{
image = GetComponent<Image>();
}
private void Update()
{
if (pinchZoom == null)
{
return;
}
// 0 = 最小ズーム、1 = 最大ズーム
float t = pinchZoom.GetNormalizedZoom();
// ズーム量に応じてアルファを補間
float alpha = Mathf.Lerp(alphaAtMinZoom, alphaAtMaxZoom, t);
Color c = image.color;
c.a = alpha;
image.color = c;
}
}
このように、「ズーム」「UIのフェード」「カメラ追従」「回転」などをそれぞれ小さなコンポーネントに分割しておくと、
ゲーム全体の設計が見通しやすくなり、プレハブ管理やレベルデザインもかなり楽になります。
ぜひ、自分のプロジェクトのカメラ周りもコンポーネント指向で整理してみてください。
