「なに切る?」ツールを作る時に設計した UI のお話。
他の記事
この記事は3番目の記事です。他の記事については以下のリンクをご覧ください。
- はじめに(概要)
- クラスの構造、実装ルール
- マウスで牌を移動、カーソル位置に戻す ← この記事
- ドラッグした牌に合わせて、他の牌を動かす
サンプル環境
- サンプル環境の牌は、記事とは別の画像になっています。
マウスで牌を移動させるための手順
次のような流れで処理を行います。
マウスで枠をクリック > ボタン押しっぱなし(OnBeginDrag)
↓
牌を動かす(OnDrag)
↓
ボタンを離す(OnEndDrag)
OnBeginDrag~OnEndDrag のイベントは勝手に実行してくれる…わけではないので、色々と手順を踏んでいきます。
下準備
EventSystem を配置
Hierarchy 上で 右クリック > UI > EventSystem
することで配置。
別の UI を配置するなどで既に EventSystem が配置されている場合は、配置する必要はありません。
カメラに Physics2DRaycaster をアタッチ
PhysicsRaycaster ではなく、Physics2DRaycaster です。
採用したのは、麻雀の盤面は平面なので、2D で事足りそう。2D の方が処理が軽いだろう、という理由です。
EventMask は Everything から UI のみに変更しておきます。
今回のケースでは Everything でも問題ありませんが、当たり判定のあるコライダーが複数重なってしまう仕様の場合、上手くやりくりする必要があるでしょう。
コライダーを枠につけるか、牌につけるか
コライダーとは、2つの動作を定義するコンポーネントです。
- 接触判定する
- 接触した後どう動かすか(玉と玉がぶつかった後どう跳ねるか、重力はどう作用するか、等)
今回の場合「コライダーとマウスカーソルの接触判定(クリックされた時)」で使います。
「接触した後どう動かすか」については今回使う必要がありません。
コライダーには 3D のものと、2D のものがあります。
先ほど説明した通り、今回の盤面は平面なので、2D のコライダーを使用します。(軽そうだし)
牌は四角いので、BoxCollider2D にしてみました。(軽そうだし)
接触判定は重い処理なので、出来るだけ軽そうなものを使っていくムーブ。
このコライダーを「枠(PaiPos)」「牌(PaiModel)」どちらにつけてもいけそうですが、「牌を置いていない場合、牌は表示されない。枠は常に存在する」という理由で、枠につけておいたほうが安全そうなので、枠につけました。
BoxCollider2D を使うために1点気を使う必要があります。
それは「BoxCollider2D は XY 平面上でしか使えない」ということです。
XZ 平面や、YZ 平面で使うことはできません。(どこにも設定が見当たらなかった)
そのため、盤面を XY 平面上に構築する必要があります。
3D は大抵 XZ 平面上がスタートなので、後で 2D コライダーをつけようと思ったら場所や角度を全部ずらす必要があった…なんて地雷を踏まないようにしましょう。
地雷、踏んだよ!!
コード
記事のコードは抜粋です。
サンプル環境に全てのコードがあるので、必要があればそちらを確認してください。
(PaiClick.cs) using UnityEngine; using UnityEngine.EventSystems; public class PaiClick : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IEndDragHandler, IDragHandler { /// <summary> /// ドラッグ開始 /// </summary> public void OnBeginDrag(PointerEventData eventData) { Debug.Log("On Begin Drag"); } /// <summary> /// ドラッグ /// </summary> public void OnDrag(PointerEventData eventData) { Debug.Log("On Drag"); } /// <summary> /// ドラッグ終了 /// </summary> public void OnEndDrag(PointerEventData eventData) { Debug.Log("On End Drag"); } /// <summary> /// クリック /// </summary> public void OnPointerClick(PointerEventData eventData) { Debug.Log("On Click"); } }
コライダーをつけたオブジェクトにこのコードをアタッチすると、マウス操作時に各イベントが呼ばれます。ドラッグ開始(OnBeginDrag) > ドラッグ中(OnDrag) > ドラッグ終了(OnEndDrag)
PointerEventData でマウスの現在位置を返すので、これを牌移動の手がかりにします。
ちなみに、ドラッグ操作をせず単にクリックすると、OnPointerClick イベントが呼ばれます。
ドラッグ操作をした場合 OnPointerClick は呼ばれません。
OnBeginDrag
PaiClick で発火したイベントは PaiPosGroup.BeginDrag に引き継がれます。
(PaiPosGroup は枠の司令塔クラスです)
(PaiPosGroup.cs) dragModel = model; dragOn(true);
dragModel は枠に置いてある牌です。OnDrag で動かす事になります。
具体的には、PaiPos に定義された Model メンバーが該当します。
この時、牌を少し大きめにして「動かしている牌をハッキリ」させています。
また、Z 値がそのままだと隣の牌と重なった時表示が荒れてしまうので、少しだけ手前に浮くようにしています。
dragOn(true) がその処理を行っています。
OnDrag
マウスのポジションを3次元座標に変換し、モデルを動かします。
マウスのポジションは2次元(XY)なので、計算によって3次元(XYZ)のオブジェクト座標を出します。
// 選択されたobjectのスクリーン座標を取得(Z 値を使う) Vector3 objectPoint = Camera.main.WorldToScreenPoint(dragModel.transform.position); // マウス位置 Vector3 pointScreen = new Vector3(mousePos.x, mousePos.y, objectPoint.z); // マウス位置を3次元座標に変換 Vector3 pointWorld = MainCamera.ScreenToWorldPoint(pointScreen); pointWorld.z = 0.1f; // モデルの位置を更新 dragModel.transform.position = pointWorld;
OnEndDrag
ドラッグしていた牌を、属している枠に戻します。
この画像だと一瞬で戻っているように見えますが、実際には減速しながら枠に戻すようにしています。
(PaiPos) IEnumerator moveToPos(PaiPos pos, PaiModel model) { var targetpos = pos.transform.localPosition; while (targetpos != Vector3.zero) { var modelpos = model.transform.localPosition; var vec = modelpos + (targetpos - modelpos) * 0.3f; var diff = targetpos - vec; if (diff.x * diff.x + diff.y * diff.y < 0.01f) { vec = targetpos; targetpos = Vector3.zero; } model.transform.localPosition = vec; yield return null; } co_move = null; }
targetpos が終端、modelpos が現在位置です。
Vector3.Lerp とかと、多分やってる事は同じです(今回は直接計算してます)。
ここでは 0.3 としていますが、この値を減らすとゆっくり、増やすとより速く終端にたどり着くことが出来ます。
お手軽でいいのですが、このアプローチはいくつか欠点があります。
- 現在位置と終端が最接近しようと「あくまで計算する」ので、いつまでたっても「=終端」にならない
- 終了時間が読みづらい
いつまでたっても終端につかない、は問題なのである程度接近したら「現在位置=終端」のように位置を確定させています。
var diff = targetpos - vec; if (diff.x * diff.x + diff.y * diff.y < 0.01f) { vec = targetpos; targetpos = Vector3.zero; }
終了時間が読みづらい、は読みづらいだけで読めなくはないのでなんとかしちゃいましょう。
(今回の例だと実行開始から targetpos が Vector3.zero になるまでの時間を実測するとか)
「終了時間が読めなくても問題ない演出に使う」というのもありですが、そういうケースは往々にして後で「やっぱ(終了時間が)必要だった」なんてことになったりします。
今回はコルーチンで処理させているので、「滅多にないが、コルーチンが終わる前に、別のコルーチンが生成されると動作が不安定になる(2つのコルーチンが互いの値を潰し合う)」という問題が予想されます。
そのため、本来なら終了時間まで演出を待つべきですが、そうすると UI の操作テンポが悪くなってしまう(恐れがある)ので、「別のコルーチン生成時に元のコルーチンを終了させる」というアプローチを取っています。
if (co_move != null) { StopCoroutine(co_move); }
たったこれだけで不具合を防げるのですが、コードをどう記述すれば安全性を確保できるかはケースバイケースなので、こういうコードがサッと思いつけると嬉しいですよね。
他人には全く評価されない