❤️

【Unity】LINEカメラの「スタンプ」のような機能を作ってみる

に公開

◆はじめに

みなさん、「LINEカメラ」は使ってますでしょうか。
様々な画像加工機能があって便利なアプリですよね。
「LINEカメラ」にはスタンプという機能がありますが、ちょっとこれに似たものを作る機会があったので、せっかくなので記事にしておこうと思います。
数学の概念が出てきますが、私も数学に堪能ではありませんので気軽に読んでいただければと思います。

◆対象読者

UnityとC#の基本的な概念がわかる初心者の方

◆どういう動きを作るか

見てもらうのが早いと思いますが、背景画像の上に重ねてスタンプ(画像)を表示し、スタンプをドラッグすれば移動でき、右下のボタンを押しながらドラッグするとそれに応じてスタンプが拡大縮小・回転するというものです。
今回はシンプルにスタンプのドラッグと、拡大縮小・回転に焦点を当てます。
LINEカメラの「スタンプ」機能

◆実践

スタンプ画像オブジェクトを作る

まずは適当なSceneを作り、Canvas(RenderMode: Screen Space - Camera)を用意します。
スタンプにする画像を用意し、Canvasの子にImageとして配置します。
(ここではStampと名付けました。)

Stampの子に、拡大縮小ボタンとなるImageを配置します。
Buttonではなく、Imageなので気をつけてください!
Scale_Rotate_Handleと名付けました)
Scale_Rotate_Handleの大きさは任意ですが、アンカーを右下にして、PositionX、PositionYを0にすることで、中心がちょうどStampの右下角にきます。

また、Stampの枠がわかりやすいように、点線の画像などを用意するのも良いでしょう。
Frameと名付けました)
この時、点線画像のアンカーを上下左右のストレッチにしておくことをお勧めします。
Stampのサイズが変わった時に、枠からずれてしまう可能性があるからです。

スタンプ画像のドラッグ機構を用意する

仕組みは簡単です。
UnityのEventSystemからドラッグ操作を受け取るためにIBeginDragHandler、IDragHandlerを実装したスクリプトを用意します。
この二つのインターフェイスを実装することで、ドラッグ開始時にOnBeginDragが呼ばれ、ドラック中にはOnDragがUpdate関数のように呼ばれます。

using UnityEngine;
using UnityEngine.EventSystems;

public class DragController : MonoBehaviour, IBeginDragHandler, IDragHandler
{
    [Header("移動対象")]
    [SerializeField] RectTransform moveTargetRectTransform;
    [Header("UIカメラ")]
    [SerializeField] Camera uiCamera;

    RectTransform parentRectTransform;
    Vector2 offsetCenterToMouseInParent;

    void Awake()
    {
        parentRectTransform = moveTargetRectTransform.parent as RectTransform;
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        //画面上のマウスの座標を、親RectTransformのローカル座標に変換
        if (RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, eventData.position, uiCamera, out var mouseLocalPosition))
        {
            //移動対象の中心位置とマウスの位置の差分
            offsetCenterToMouseInParent = moveTargetRectTransform.anchoredPosition - mouseLocalPosition;
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        //計算失敗時は何もしない
        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRectTransform, eventData.position, uiCamera, out var mouseLocalPosition))
        {
            return;
        }

        //ドラッグ開始時の相対位置を保ったまま、移動対象の中心位置を更新
        Vector2 newCenter = mouseLocalPosition + offsetCenterToMouseInParent;
        moveTargetRectTransform.anchoredPosition = newCenter;
    }
}

このスクリプトをStampにアタッチします。
インスペクタからmoveTargetRectTransformに自分自身のRectTransformをアタッチすることも、お忘れなく。

拡大縮小・回転機構を用意する

こちらも、IBeginDragHandler、IDragHandlerを実装してスクリプトを用意します。

using UnityEngine;
using UnityEngine.EventSystems;

public class ResizeRotateHandle : MonoBehaviour, IBeginDragHandler, IDragHandler
{
    [Header("拡大・回転する対象")]
    [SerializeField] RectTransform resizeRotateTargetRectTransform;
    [Header("UIカメラ")]
    [SerializeField] Camera uiCamera;

    RectTransform parentRect; //拡大・回転する対象の親(座標系の基準)
    //ターゲットの基準(初期)サイズ
    Vector2 initialSize;
    //中心→右下角の基準ベクトルu
    Vector2 u;
    //ベクトルuの長さ
    float uLength;

    void Awake()
    {
        parentRect = resizeRotateTargetRectTransform.parent as RectTransform;
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        //基準サイズと基準ベクトル
        initialSize = resizeRotateTargetRectTransform.rect.size;
        u = new Vector2(initialSize.x * 0.5f, -initialSize.y * 0.5f); //pivotが中心(0.5、0.5)の時のベクトル算出
        uLength = u.magnitude;
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(parentRect, eventData.position, uiCamera, out var mouseLocalPosition))
        {
            return;
        }

        //ターゲットの中心→マウスポイントへのベクトルv
        Vector2 center = resizeRotateTargetRectTransform.anchoredPosition;
        Vector2 v = mouseLocalPosition - center;
        float vLength = v.magnitude;
        
        //ベクトルvが極端に短い場合は無視(ゼロ除算防止)
        if (vLength < 1e-4f)
        {
            return;
        }

        //回転:基準ベクトルuとvの角度でターゲットを回転
        float angle = Vector2.SignedAngle(u, v);
        var localEulerAngles = resizeRotateTargetRectTransform.localEulerAngles;
        localEulerAngles.z = angle;
        resizeRotateTargetRectTransform.localEulerAngles = localEulerAngles;

        //スケール:ベクトルvとベクトルuの長さでスケールを算出し、サイズに反映
        float scale = vLength / uLength;
        //幅と高さに同じだけかけて比率維持
        float newWidth = initialSize.x * scale;
        float newHeight = initialSize.y * scale;

        //アンカー尊重で安全にサイズ変更
        resizeRotateTargetRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, newWidth);
        resizeRotateTargetRectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, newHeight);
    }
}

このスクリプトはScale_Rotate_Handleにアタッチします。

動作確認

LINEカメラのスタンプ機能のような挙動が実現できました。

◆おわりに

今回はScene上にただ配置したStampを動かしましたが、StampをPrefab化し、必要な時に動的に生成するようにすれば、ユーザーが自由に選んだ画像でStampを作ることができます。
Stampの右上にさらに×ボタンを用意したり、編集可能状態のオンオフ機能も用意すれば、立派なスタンプ機能になりそうです。

Discussion