Unityを触り始めた頃って、つい何でもかんでも Update() に書いてしまいがちですよね。タッチ入力の処理、スワイプ判定、プレイヤーの移動、エフェクトの再生…全部ひとつのスクリプトに押し込むと、最初は動いていても、だんだん「どこを直せばいいのかわからない巨大スクリプト(Godクラス)」になってしまいます。
特にスマホ向けのゲームでありがちなのが、「入力処理」と「ゲームのロジック」をベタ書きで混ぜてしまうパターンです。例えば「スワイプしたらプレイヤーを動かす」処理を、プレイヤーのスクリプトの Update() に直接書いてしまうと、入力方式を変えたいだけでもプレイヤーのコードをいじる羽目になります。
そこで今回は、「スワイプを検知するだけ」に責務を絞ったコンポーネントを用意して、
ゲーム側とは「イベント(シグナル)」でゆるく連携するスタイルを紹介します。
【Unity】タッチ操作をスマートに分離!「SwipeDetector」コンポーネント
ここで作る SwipeDetector コンポーネントは、
- タッチ(またはマウス)で指を押した位置と離した位置を記録
- 一定距離以上動いていたらスワイプと判定
- 上下左右の4方向を判定して、C#イベントで通知
という、かなり「入力検知」に特化した小さなコンポーネントです。
プレイヤーやUIは、このイベントを購読して「スワイプされたらこう動く」というロジックだけを書けばOK、という構成にしていきましょう。
フルコード:SwipeDetector.cs
using System;
using UnityEngine;
/// <summary>スワイプ方向を表す列挙体</summary>
public enum SwipeDirection
{
None,
Up,
Down,
Left,
Right
}
/// <summary>
/// タッチ(またはマウス)によるスワイプを検知し、
/// 上下左右の方向をイベントで通知するコンポーネント。
/// ゲームロジックとはイベント経由で疎結合に連携するのがポイント。
/// </summary>
public class SwipeDetector : MonoBehaviour
{
[Header("スワイプ判定設定")]
[SerializeField]
private float minSwipeDistance = 50f;
// 画面上で「この距離以上」動いたらスワイプとみなす(ピクセル相当)
[SerializeField]
private float maxSwipeTime = 1.0f;
// この時間以内の操作だけをスワイプとして扱う(長押しと区別したい場合に使用)
[Header("入力モード")]
[SerializeField]
private bool useMouseAsTouch = true;
// PC上でテストしやすいように、マウスドラッグでもスワイプとして扱うかどうか
// スワイプ開始位置(スクリーン座標)
private Vector2 _startPosition;
// スワイプ開始時刻
private float _startTime;
// 現在タッチ中かどうか
private bool _isSwiping;
/// <summary>
/// スワイプが完了したときに発火するイベント。
/// 引数は検知されたスワイプ方向。
/// </summary>
public event Action<SwipeDirection> OnSwipeDetected;
private void Update()
{
// 実行環境によって入力を切り替える
if (Input.touchSupported && Input.touchCount > 0)
{
HandleTouchInput();
}
else if (useMouseAsTouch)
{
HandleMouseInput();
}
}
/// <summary>
/// タッチ入力からスワイプを検知
/// </summary>
private void HandleTouchInput()
{
Touch touch = Input.GetTouch(0);
switch (touch.phase)
{
case TouchPhase.Began:
_isSwiping = true;
_startPosition = touch.position;
_startTime = Time.time;
break;
case TouchPhase.Moved:
case TouchPhase.Stationary:
// 移動中 or 押しっぱなし中は特に何もしない
break;
case TouchPhase.Ended:
case TouchPhase.Canceled:
if (_isSwiping)
{
EvaluateSwipe(touch.position, Time.time - _startTime);
}
_isSwiping = false;
break;
}
}
/// <summary>
/// マウス入力からスワイプを検知(エディタ/PC用)
/// </summary>
private void HandleMouseInput()
{
// 左クリック押し始め
if (Input.GetMouseButtonDown(0))
{
_isSwiping = true;
_startPosition = Input.mousePosition;
_startTime = Time.time;
}
// 左クリックを離したとき
if (Input.GetMouseButtonUp(0))
{
if (_isSwiping)
{
Vector2 endPosition = Input.mousePosition;
float duration = Time.time - _startTime;
EvaluateSwipe(endPosition, duration);
}
_isSwiping = false;
}
}
/// <summary>
/// 開始位置・終了位置・経過時間からスワイプかどうかを判定し、
/// スワイプなら方向を計算してイベントを発火する。
/// </summary>
/// <param name="endPosition">スワイプ終了位置(スクリーン座標)</param>
/// <param name="duration">スワイプにかかった時間(秒)</param>
private void EvaluateSwipe(Vector2 endPosition, float duration)
{
// 長すぎる操作はスワイプ扱いしない(長押しなど)
if (duration > maxSwipeTime)
{
return;
}
Vector2 delta = endPosition - _startPosition;
float distance = delta.magnitude;
// 距離が足りない場合はスワイプではない
if (distance < minSwipeDistance)
{
return;
}
// どの方向が強いかで上下左右を判定
SwipeDirection direction;
// X方向とY方向、どちらの絶対値が大きいかで「横スワイプか縦スワイプか」を決定
if (Mathf.Abs(delta.x) > Mathf.Abs(delta.y))
{
// 横方向のスワイプ
direction = delta.x > 0 ? SwipeDirection.Right : SwipeDirection.Left;
}
else
{
// 縦方向のスワイプ
direction = delta.y > 0 ? SwipeDirection.Up : SwipeDirection.Down;
}
// イベント購読者がいれば通知
OnSwipeDetected?.Invoke(direction);
}
/// <summary>
/// 外部からスワイプ検知イベントを購読しやすくするためのヘルパー。
/// 例: swipeDetector.AddListener(dir => Debug.Log(dir));
/// </summary>
/// <param name="listener">スワイプ方向を受け取るコールバック</param>
public void AddListener(Action<SwipeDirection> listener)
{
OnSwipeDetected += listener;
}
/// <summary>
/// 購読解除用のヘルパー。
/// </summary>
public void RemoveListener(Action<SwipeDirection> listener)
{
OnSwipeDetected -= listener;
}
}
使い方の手順
-
コンポーネントを配置する
- 空の GameObject を作成して名前を「InputManager」などにします。
SwipeDetectorスクリプトをそのオブジェクトにアタッチします。- インスペクターで
Min Swipe Distance(例: 50〜100)Max Swipe Time(例: 0.8〜1.0)Use Mouse As Touch(PCテスト時はチェックON)
をお好みで調整します。
-
プレイヤーやUI側でイベントを購読する
例として、スワイプ方向にプレイヤーを1マス動かすコンポーネントを作ります。using UnityEngine; /// <summary> /// SwipeDetector からのスワイプ方向を受け取り、 /// プレイヤーをグリッド状に移動させる簡単な例。 /// </summary> [RequireComponent(typeof(Transform))] public class SwipeMovePlayer : MonoBehaviour { [SerializeField] private SwipeDetector swipeDetector; // Hierarchy 上の SwipeDetector をドラッグ&ドロップでセット [SerializeField] private float moveStep = 1f; // 1回のスワイプでどれだけ動くか private void Awake() { if (swipeDetector == null) { // 近くの SwipeDetector を自動取得(なければ手動でセットしてもOK) swipeDetector = FindAnyObjectByType<SwipeDetector>(); } } private void OnEnable() { if (swipeDetector != null) { swipeDetector.AddListener(OnSwiped); } } private void OnDisable() { if (swipeDetector != null) { swipeDetector.RemoveListener(OnSwiped); } } private void OnSwiped(SwipeDirection direction) { Vector3 offset = Vector3.zero; switch (direction) { case SwipeDirection.Up: offset = Vector3.up * moveStep; break; case SwipeDirection.Down: offset = Vector3.down * moveStep; break; case SwipeDirection.Left: offset = Vector3.left * moveStep; break; case SwipeDirection.Right: offset = Vector3.right * moveStep; break; } transform.position += offset; } }- プレイヤーの GameObject に
SwipeMovePlayerをアタッチ。 SwipeDetectorフィールドに、先ほど作った InputManager の SwipeDetector をドラッグして関連付けます。
- プレイヤーの GameObject に
-
敵や動く床など、別のオブジェクトでも再利用する
例えば「スワイプした方向にだけ動く床」を作る場合は、別のコンポーネントで同じようにイベントを購読するだけです。using UnityEngine; /// <summary> /// スワイプ方向に応じて ON/OFF を切り替える床の例。 /// 上スワイプで有効、下スワイプで無効にする。 /// </summary> public class SwipeTogglePlatform : MonoBehaviour { [SerializeField] private SwipeDetector swipeDetector; [SerializeField] private GameObject platformVisual; // 実際の床の見た目(SpriteRenderer 付きオブジェクトなど) private void Awake() { if (swipeDetector == null) { swipeDetector = FindAnyObjectByType<SwipeDetector>(); } } private void OnEnable() { if (swipeDetector != null) { swipeDetector.AddListener(OnSwiped); } } private void OnDisable() { if (swipeDetector != null) { swipeDetector.RemoveListener(OnSwiped); } } private void OnSwiped(SwipeDirection direction) { if (direction == SwipeDirection.Up) { platformVisual.SetActive(true); } else if (direction == SwipeDirection.Down) { platformVisual.SetActive(false); } } }同じ
SwipeDetectorから、複数のオブジェクトが好き勝手にイベントを受け取れるので、入力とゲームロジックをきれいに分離できます。 -
UIのデバッグ用にログを出しておく
開発中は、スワイプが正しく検知されているか確認したい場面が多いので、専用の「ログ表示コンポーネント」を1つ作っておくと便利です。using UnityEngine; /// <summary> /// スワイプ方向をコンソールにログ出力するだけのデバッグ用コンポーネント。 /// </summary> public class SwipeLogger : MonoBehaviour { [SerializeField] private SwipeDetector swipeDetector; private void Awake() { if (swipeDetector == null) { swipeDetector = FindAnyObjectByType<SwipeDetector>(); } } private void OnEnable() { if (swipeDetector != null) { swipeDetector.AddListener(LogSwipe); } } private void OnDisable() { if (swipeDetector != null) { swipeDetector.RemoveListener(LogSwipe); } } private void LogSwipe(SwipeDirection direction) { Debug.Log($"Swipe detected: {direction}"); } }こうして「検知」「移動」「可視化」をバラしておくことで、それぞれの責務がはっきりします。
メリットと応用
SwipeDetector を入力専用のコンポーネントとして切り出すメリットはかなり大きいです。
- プレハブがシンプルになる
プレイヤーのプレハブから「入力処理」を追い出せるので、プレイヤー側は「どう動くか」だけに集中できます。
入力方式をスワイプからボタンに変えたくなったときも、プレイヤーのスクリプトを触らずに済みます。 - レベルデザインが楽になる
スワイプに反応するギミック(動く床、敵のパターン切り替え、カメラの切り替えなど)を、それぞれ独立したコンポーネントとして作れるので、ステージごとに「どのスワイプで何が起こるか」を組み合わせるだけでレベルデザインができます。 - テストとデバッグがしやすい
入力のテストはSwipeDetectorだけ見ればよく、ゲームロジックのバグとは切り分けて考えられます。
逆に「スワイプ検知はOKだが、プレイヤーが動かない」という場合は、プレイヤー側だけを確認すればいいので、原因の切り分けがかなり楽になります。
応用として、例えば「一定時間内に連続スワイプしたら必殺技」みたいな仕様も、SwipeDetector のイベントを別のコンポーネントで監視するだけで実現できます。
最後に、「スワイプの強さ(距離)に応じて移動量を変える」改造案の例を載せておきます。
SwipeDetector 側で距離も一緒に通知する設計に変えるのもアリですが、シンプルに「生の入力座標から自前で計算する」方法もあります。
/// <summary>
/// スワイプ方向だけでなく、スワイプ距離に応じて移動量を変える例。
/// SwipeDetector の OnSwipeDetected とは別に、
/// 生の入力座標を使って独自に距離を計算するパターン。
/// </summary>
private void MoveWithSwipePower(Vector2 startScreenPos, Vector2 endScreenPos)
{
// スクリーン座標の差分
Vector2 delta = endScreenPos - startScreenPos;
// 距離に応じて 0〜1 に正規化(任意のスケール)
float maxExpectedDistance = 300f; // 想定する最大スワイプ距離
float power = Mathf.Clamp01(delta.magnitude / maxExpectedDistance);
// ワールド座標での移動方向(ここでは単純に右方向にのみ適用)
Vector3 moveDir = Vector3.right;
// スワイプの強さで移動量をスケーリング
float maxMoveDistance = 5f;
Vector3 moveOffset = moveDir * (maxMoveDistance * power);
transform.position += moveOffset;
}
このように、「入力を検知するコンポーネント」 と 「入力に反応して動くコンポーネント」 を分けておくと、仕様変更や拡張にかなり強い構成になります。
巨大な Update() に書き足していくのではなく、小さな責務のコンポーネントを組み合わせるスタイルを意識してみてください。
