スナップ処理とは、UI によくある、何かのオブジェクトにある程度近づくと、くっつく(ように見える)処理です。
正確には、一定の場所に留める処理です。
本稿ではスナップ処理を Unity で作る方法について説明します。
細かい点までは説明していませんので、自分ひとりで Unity を使ってゲームを作れる中級者以上の方を対象としています。
執筆時に使用した Unity のバージョンは、2021.2.3f1
Windows10(Home) 64bit 版で作成し、動作確認しています。
ワールドマップや家の壁設置なんかでスナップ処理ってけっこう使うので、ぱっと作れるように汎用スクリプトを作成😆
スナップ処理より、新InputSystemでマウスとゲームパッド制御する方法とか、このプロジェクトで使えるアウトラインシェーダー探したりとかの方に時間がかかりました😅#Unity pic.twitter.com/2ktDR25RsM— おりが@ゲーム製作(Unity) (@dokuro_moe) November 22, 2021
- 操作しているオブジェクト(A)とスナップ対象のオブジェクト(B)が一定範囲内まで接近したら、A を B の位置+オフセットに転送する。
 - 転送後、移動操作を行っても、一定時間、A が移動しなくなる(スナップする)。
 - 転送後、A が B の一定範囲外へ移動するまで、1. を行わない。
 
具体的な範囲(距離)は、ソースコードを修正することなく、変更可能にする。
カーソルをアイコンと同じ位置に重ねて表示したい場合もありますが、ずらして表示したい場合もあるので、その際、「オフセット」にずらす量を指定する。
オフセットもソースコードを修正することなく、変更可能にする。
具体的な時間(秒)は、ソースコードを修正することなく、変更可能にする。
1. の一定範囲内と同じ値を使用する。
- スナップ処理を管理するクラス
 - カーソルの位置を取得、変更する
 - カーソルとスナップ対象が接触しているか調べる
 - スナップ状態の取得、設定
 - 一定時間経過したかを調べる
 
// カーソルをスナップしたいオブジェクトに AddComponent する。
using UnityEngine;
public class CursorSnap : MonoBehaviour
{
  public GameObject cursor = null;
  public Camera use_camera = null;
  public Vector2 snap_offset;
  public float hold_seconds = 0.5f;
  float holding_seconds = 0.0f;
  enum HoldState
  {
    Free,
    Holding,
    Finished,
  }
  HoldState state = HoldState.Finished;
  bool is_colliding = false;
  void Update()
  {
    if (cursor == null || use_camera == null) { return; }
    if (state == HoldState.Finished)
    {
      if (is_colliding)
      {
        HoldCursor();
        holding_seconds = hold_seconds;
        state = HoldState.Holding;
      }
    }
    else
    {
      if (state == HoldState.Holding)
      {
        holding_seconds -= Time.deltaTime;
        HoldCursor();
        state = holding_seconds <= 0.0f ?
          HoldState.Free : HoldState.Holding;
      }
      else
      {
        state = is_colliding ? HoldState.Free : HoldState.Finished;
      }
    }
  }
  // カーソルの位置をこのオブジェクトの位置に固定する
  void HoldCursor()
  {
    var v = transform.position;
    var p = cursor.transform.position;
    cursor.transform.position =
      new Vector3(v.x + snap_offset.x, v.y + snap_offset.y, p.z);
  }
  // カーソルとオブジェクトの衝突判定
  void OnTriggerEnter2D(Collider2D _)
  {
    is_colliding = true;
    GetComponent<IconAnimation>()?.Play();
    cursor.transform.GetComponent<CursorAnimation>()?.Stop();
  }
  void OnTriggerExit2D(Collider2D _)
  {
    is_colliding = false;
    GetComponent<IconAnimation>()?.Stop();
    cursor.transform.GetComponent<CursorAnimation>()?.Play();
  }
}
// InputSystem からカーソル位置を取得してゲームカーソルに反映する
// ゲームカーソルとして使う GameObject に AddComponent する
using UnityEngine;
using UnityEngine.InputSystem;
public class CursorMove : MonoBehaviour
{
  public Camera use_camera = null;
  public GameObject cursor = null;
  public float pad_speed_x = 0.5f;
  public float pad_speed_y = 0.5f;
  public void OnPoint(InputAction.CallbackContext context)
  {
    if (context.control == null ||
        context.control.device == null ||
        use_camera == null ||
        cursor == null
    ) { return; }
    var p = context.ReadValue<Vector2>();
    is_gamepad = false;
    if (context.control.device is Pointer)
    {
      var v = use_camera.ScreenToWorldPoint(
        new Vector3(p.x, p.y, Mathf.Abs(use_camera.transform.position.z))
      );
      v.z = 0;
      cursor.transform.position = v;
    }
    else if (context.control.device is Gamepad)
    {
      // ゲームパッドは値が変わったときしかコールバックが来ない
      // みたいなので変化量を保持して Update() でカーソルを移動させる。
      is_gamepad = true;
      leftstick = p;
    }
  }
  Vector3 leftstick = Vector3.zero;
  bool is_gamepad = false;
  void Update()
  {
    if (use_camera == null || cursor == null) { return; }
    if (is_gamepad)
    {
      var p = use_camera.WorldToScreenPoint(cursor.transform.position);
      p.x += leftstick.x * pad_speed_x;
      p.y += leftstick.y * pad_speed_y;
      var v = use_camera.ScreenToWorldPoint(
        new Vector3(p.x, p.y, Mathf.Abs(use_camera.transform.position.z))
      );
      v.z = 0;
      cursor.transform.position = v;
    }
  }
}
新しい InputSystem を使用しています。
空のオブジェクトに Player Input を AddComponent

UI > Actions > Point に Left Stick を追加し、ゲームパッドの左アナログスティックでカーソル移動できるようにします。

Player Input に戻り、UI の Point イベントにコールバック先を追加。
UI 以外は使用しないので、Default Map を UI に変更。

2D コライダーを使うのが楽です。
Rigidbody2D はカーソル側にだけ追加します。
「トリガーにする」をチェックしたら、void OnTriggerEnter2D(Collider2D) と void OnTriggerExit2D(Collider2D) で接触したときと、離れたときを検出できます。
アイコン側には Rigidbody2D は必要ありません。
enum HoldState
{
  Free,
  Holding,
  Finished,
}
HoldState state = HoldState.Finished;
enum で管理。
Free = スナップの待ち時間終了
Holding = スナップ中
Finished = スナップ終了
アイコン(くっつけられる)側のインスペクター

コライダーを追加して、スナップ処理スクリプト(CursorSnap)を追加。
スナップ処理スクリプトに、ゲームカーソルとして使う GameObject と、カメラを設定(ワールド座標→スクリーン座標変換用)。
Snap_Offset は、カーソルをスナップしたときに、カーソルを固定させる位置を指定します。
アイコンの中心点からのオフセット(相対)位置を指定します。
Holding_Seconds は、カーソルをスナップしたときに、カーソルの移動を停止させる秒数を指定します。
私の感覚だと、0.5 ~ 1.0 秒くらいが許容範囲です。
スナップを一時的に止めたいときは、CursorSnap を停止させます。
Update() で毎フレーム処理しているので、Unity ではお馴染みの Time.deltaTime を使います。
アイコンは以下の有料アセットを使用しました。
背景の画像は以下からダウンロードしました。
3Dオブジェクトにアウトラインを付けるシェーダーは以下を使用しました。
有料アセットと、再配布可能か未確認の素材が含まれるため、それらを含まないプロジェクトを作成しました。
本稿では要点しか説明していませんので、詳細はプロジェクトをダウンロードし、内容をご確認下さい。

	



