UI、つまみの作成

はじめに

画面上で動かせるつまみを作ってみる。

スクリプト

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Slider))]
public class KnobController : MonoBehaviour
{
    // スライダーのDirectionはTopToBottom
    // 上に動かせばプラスの値、下に動かせばマイナスの値
    [SerializeField] Slider _slider;

    void Start()
    {
        OnDragEnd();
    }

    /// <summary>
    /// スライダーが動かされたら呼び出される
    /// </summary>
    public void OnValueChanged()
    {
        // つまみが移動された分をどこかに渡す
        var result = _slider.maxValue / 2 - _slider.value;
        // manager.KnobValueChange(result);
    }

    /// <summary>
    /// 離したらスライダーの中心にもどる
    /// </summary>
    public void OnDragEnd()
    {
        _slider.value = _slider.maxValue / 2;
    }
}

説明

unityのUIにあるスライダーをもとにそれっぽくスクリプトで制御している。

おわりに

中身は簡単だがつまみのように見せるには、アニメーションや効果音などの操作感を演出する必要がある。

三角関数で画面端の座標を求める

はじめに

画面端の座標を決めておいてプレイヤーの狙った先の画面端の座標を求める。

方法

yoshipenguins.hatenablog.com

前回の記事の方法で取得した角度をもとにプレイヤーから狙った画面端の座標を三角関数を使って計算する。

スクリプト

public void TakeAim()
{
    // 的の親オブジェクトを回転させる
    var angle = GameManager.Instance.GetHandleAngle();
    _targetRoot.eulerAngles = new Vector3(0, 0, angle);

    // 的とのベクトル差
    var vecDiff = _target.position - transform.position;

    _aimPos = Vector2.one;
    var rad = GameManager.Instance.GetHandleRadian();

    // x座標が画面端だと仮定してy座標を求める
    var limitX = vecDiff.x >= 0 ? _edgeHi.x : _edgeLo.x;
    limitX -= transform.position.x;
    _aimPos.y = limitX * Mathf.Tan(rad);
    if (_aimPos.y <= _edgeHi.y - transform.position.y &&
        _aimPos.y >= _edgeLo.y - transform.position.y)
    {
        _aimPos.x = limitX;
    }
    else
    {
        var limitY = vecDiff.y >= 0 ? _edgeHi.y : _edgeLo.y;
        limitY -= transform.position.y;
        _aimPos.x = limitY / Mathf.Tan(rad);
        _aimPos.y = limitY;
    }

    // プレイヤーの位置を基準にする
    _aimPos += (Vector2)transform.position;
    // 予測した座標を代入
    var pos = new Vector3[] { transform.position, _aimPos };
    _renderer.SetPositions(pos);
}

説明

14行目からの処理が重要。
三角関数から y=xtanθ と式を入れ替えて x=y/tanθ を使い座標を求める。
処理では先にy座標を求める。x座標は画面端だと仮定しlimitXに代入する。この時プレイヤーの座標を引いているのはプレイヤーからみた画面端までのベクトルにするため。


式によって求めたy座標が画面範囲外ならy座標を画面端と決めてx座標を計算する。
そして範囲内の座標を計算できたらプレイヤーの座標を加えて位置を調整する。

おわりに

sinとcosを使う場合は斜辺の長さが必要なのだが長さを計算は処理が重い。斜辺を使わないことで少し行が長くなってしまったが、この処理は毎フレーム行うので効果はあると思う。ただどこかでsin、cos、tanの関数も重いらしいと聞いた。

参考にしたもの

【数学】ゲーム開発で覚えておくべき三角関数の性質 - LIGHT11

画面上で回せるハンドルを作る

はじめに

スクリーン上でくるくると回せるハンドルを実装します。マウスで操作できます。


エディターでの作業

キャンバス上にハンドルとなるImageを作成します。
そのハンドルにスクリプトとEventTriggerを取り付けておきます。

EventTriggerのDragイベントにOnDrag関数を、
BeginDragイベントにOnBeginDrag関数をそれぞれ設定する。
これはハンドル上でドラッグしたときに呼び出されます。

スクリプト

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;

public class HandleController : MonoBehaviour
{
    const float HALF_ROUND = 180;
    const float ROUND = 360;
    float _handleUpperLimit = 360 * 8;
    float _lowerLimit = 0;
    float _aimUpperLimit = 360;

    // 回転を始めた角度を基準に現在ハンドルが回転した角度
    float _currentAngle = 0;
    // 回した直前の角度と回転方向
    float _beforeAngle = 0;
    // ハンドルがいままで回転した角度(総回転角)
    [SerializeField] float _totalAngle = 0;

    void Start()
    {
        // ハンドルの角度の初期設定。
        var rot = transform.eulerAngles;
        rot.z = ClampTotalAngle(0) % ROUND;
        transform.eulerAngles = rot;
    }

    // 角度差を制限内で総回転角に加算
    float ClampTotalAngle(float dt)
    {
        return Mathf.Clamp(_totalAngle + dt, _lowerLimit, _handleUpperLimit);
    }

    // ハンドルの回し始めを直前の角度にする
    public void OnBeginDrag()
    {
        _beforeAngle = GetMouseAngle();
    }

    // 回した直前の角度と現在の角度との差を総回転角に代入する
    public void OnDrag()
    {
        var rot = transform.eulerAngles;
        _currentAngle = GetMouseAngle();

        // 角度差を計算
        float deltaAngle = _currentAngle - _beforeAngle;
        deltaAngle = HALF_ROUND >= Mathf.Abs(deltaAngle) ?
            deltaAngle : -Mathf.Sign(deltaAngle) * (ROUND - Mathf.Abs(deltaAngle));

        // 角度差を加算して角度制限
        _totalAngle = ClampTotalAngle(deltaAngle);

        // ハンドルの更新
        rot.z = _totalAngle % ROUND;
        transform.eulerAngles = rot;

        // 現在の角度をキャッシュ
        _beforeAngle = _currentAngle;
    }

    // ハンドルからみたマウスの位置の角度を返す
    float GetMouseAngle()
    {
        var mousePos = Vector3.zero;
        // スクリーン座標からワールド座標に変換
        RectTransformUtility.ScreenPointToWorldPointInRectangle(
            transform as RectTransform, Mouse.current.position.ReadValue(), Camera.main, out mousePos);
        var dt = mousePos - transform.position;
        // ハンドルから見たマウスの角度(値が-180~180の間になってる)
        var deg = Mathf.Atan2(dt.y, dt.x) * Mathf.Rad2Deg;
        // 360°に変換
        var mouseAngle = deg < 0 ? deg + 360 : deg;
        return mouseAngle;
    }

    /// <summary>
    /// プレイヤーが狙う角度を返す
    /// </summary>
    /// <returns>角度</returns>
    public float AimAngle()
    {
        var angle = (_totalAngle / _handleUpperLimit) * _aimUpperLimit;
        return angle;
    }
}

説明

実行結果

ハンドル上でドラッグされてる間、ハンドルを基準としたマウスの角度をGetMouseAngle関数で取り続けます。
ハンドルを回しているのはOnDrag関数です。
まず現在フレームと前フレームの角度差を計算します。

// 角度差を計算
float deltaAngle = _currentAngle - _beforeAngle;
deltaAngle = HALF_ROUND >= Mathf.Abs(deltaAngle) ?
    deltaAngle : -Mathf.Sign(deltaAngle) * (ROUND - Mathf.Abs(deltaAngle));

ここの処理で角度差は180°以下だとみなしています。それ以上の場合は逆方向に回転したと処理します。
こうすることでどちらに回転しても角度差をそのまま利用できます。基準のX軸をまたぐ時には回転方向を合わせるため符号を逆にしています。

その後は角度差をtotalAngleに加算して360で割った値をハンドルの回転に代入してます。

AimAngle関数ではハンドルの総回転角をハンドルの上限で割って狙っている角度を返します。 handleUpperLimitを変更すると狙う角度の上限までに必要なハンドルの回転角度が変わります。

おわりに

飛び道具の軌道予測のために角度を毎フレーム取得し続けています。
そこまで処理コストは高くないと思うけどもっと良い方法があったら教えていただきたい。

参考にしたもの

DSiソフトウェア「ねらってスポっと!」のUI