【Unity】スナップ処理を作る(2D用)

スナップ処理とは、UI によくある、何かのオブジェクトにある程度近づくと、くっつく(ように見える)処理です。
正確には、一定の場所に留める処理です。

本稿ではスナップ処理を Unity で作る方法について説明します。
細かい点までは説明していませんので、自分ひとりで Unity を使ってゲームを作れる中級者以上の方を対象としています。

執筆時に使用した Unity のバージョンは、2021.2.3f1
Windows10(Home) 64bit 版で作成し、動作確認しています。

スナップ処理の要件定義
  1. 操作しているオブジェクト(A)とスナップ対象のオブジェクト(B)が一定範囲内まで接近したら、A を B の位置+オフセットに転送する。
  2. 転送後、移動操作を行っても、一定時間、A が移動しなくなる(スナップする)。
  3. 転送後、A が B の一定範囲外へ移動するまで、1. を行わない。
1. 一定範囲内

具体的な範囲(距離)は、ソースコードを修正することなく、変更可能にする。

1. 位置+オフセット

カーソルをアイコンと同じ位置に重ねて表示したい場合もありますが、ずらして表示したい場合もあるので、その際、「オフセット」にずらす量を指定する。
オフセットもソースコードを修正することなく、変更可能にする。

2. 一定時間

具体的な時間(秒)は、ソースコードを修正することなく、変更可能にする。

3. 一定範囲外

1. の一定範囲内と同じ値を使用する。

要件を満たすために必要なもの
  1. スナップ処理を管理するクラス
  2. カーソルの位置を取得、変更する
  3. カーソルとスナップ対象が接触しているか調べる
  4. スナップ状態の取得、設定
  5. 一定時間経過したかを調べる
スナップ処理を管理するクラス
// カーソルをスナップしたいオブジェクトに 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 コライダーを使うのが楽です。

カーソル(くっつける)側の設定(A)

Rigidbody2D はカーソル側にだけ追加します。

アイコン(くっつけられる)側の設定(B)

「トリガーにする」をチェックしたら、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オブジェクトにアウトラインを付けるシェーダーは以下を使用しました。

今回作成したプロジェクトについて

有料アセットと、再配布可能か未確認の素材が含まれるため、それらを含まないプロジェクトを作成しました。
本稿では要点しか説明していませんので、詳細はプロジェクトをダウンロードし、内容をご確認下さい。

ダウンロード(Google Drive)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です