ガンズターン 公式サイト

楽しいことに、まじめです。 ——ガンズターンアプリ研究所公式サイト

Unity備忘録 #2 uGUIでスライドビュー作ってみた

Pocket

0. 引き続きUnityしてます

お疲れ様です。ガンズターンのRyosukeです。
相変わらずUnityの勉強を兼ねつつ、次回作の作成をコツコツと進めております。

さて。次回作でのUIにおいて、どうしてもスライドビュー(正式名称わからないんですが、横にスライドするとその位置に応じてボタンのサイズが動的に変わるやつ)を作りたくて、今日一日でなんとか形になりましたので、恥ずかしいですがお披露目します。

一体なにを作りたかったというと、こんなやつです。

スライドビューのgifアニメ

こんなのを作りたかったんです

macのファインダーの表示みたいなやつです。
こいつを、uGUIを使って、どうしてもアプリ内で実装したかったんですね。

AssetStoreを探せばすでに似たようなやつありそうですが、これぐらい自前でも作れるだろ、と高をくくっていた挙句、この実装のためだけに1日使ってしまいました。

これからこのUIをどう作ったか、解説していきたいと思いますが、はっきり言って素人のプログラムですんで、玄人の方から見たら物足りないかもしれません。
(自分としてみても、課題はまだまだあります。できれば、固定のリストじゃなくて、無限スクロールにしたいんです)

そのあたり、もっとよい方法があるよ、とか何かあったら教えて頂けたらとっても嬉しいです。

んでは、さっそく始めます。

1. とりあえずSlideViewControllerを作る

まず、UnityのシーンにCanvasを追加して、その中にPanelを新規追加します。
(Panelじゃなくて空のGameObjectとかでも問題ないです)

そしたらそのPanelの名前を「SlideViewController」にします。(わかりやすければなんでもいいです)

こいつの下に、スライドするボタンが並ぶイメージです。

2. SlideViewControllerの下にボタンを並べる

ボタンのサイズと表示位置は、後に作るスクリプト内で動的に指定するので、とりあえずエディタ上で操作しやすい位置にいくつかボタンを並べます。
代表となるボタンをprefab化して、Duplicateしてもいいと思います。
1つだけ注意点があるとしたら、全てのボタンのWidthとHeightを合わせておいたほうがよいということぐらいです(見栄え的に)。ちなみにわたしはWidthとHeightともに80でまず試しました。

ちなみに、今回作るスライドビューでは、横に並ぶ項目の数は固定です。
ここで作ったボタンの数だけ、横にずらっと並ぶことになります。
とりあえず、10個以上あればそれなりにスライドビューを楽しめると思います。
(いずれ、動的にリストを作成して無限スクロールをできるものに改良しようと思います)

3. SlideViewControllerスクリプトを作成する

こんなスクリプトを作成します。(C#です。ベタ打ちです。長いです。ごめんなさい)

using UnityEngine;
using UnityEngine.UI;
using System;
using System.Collections;

public class SlideViewController : MonoBehaviour 
{
    [SerializeField]
    private Button[] buttons; // スライドするボタンを全て登録
    [SerializeField]
    private float centerX; // スライドの中心のX座標
    [SerializeField]
    private float centerY; // スライドの中心のY座標
    [SerializeField]
    private float baseZ; // スライドの中心のZ座標
    [SerializeField]
    private float backgroundWidth; // 背景の幅(最小サイズになる位置の計算に用いる)
    [SerializeField]
    private float maxSize; // 最大サイズ
    [SerializeField]
    private float minSize; // 最小サイズ
    [SerializeField]
    private float distance; // 隣のボタンからの距離(一定)
    [SerializeField]
    private float dampingRatio; //慣性スライド時の減衰率
    
    float originX, oldOriginX; // 左端のボタンのx座標(その他のボタンの基準位置となる)

    //ドラッグ検知用
    Vector3 startPos; // エディタ上での確認時にのみ用いる
    Vector3 deltaPos;

    void Start () 
    {
        originX = 0.0f;
        oldOriginX = 0.0f;
        UpdateSizesAndPositions();
    }
    
    void Update () 
    {

        if (Application.isEditor) //エディタ上でのデバッグ時に動作するコード
        {
            // マウス左ボタン押された時
            if (Input.GetMouseButtonDown(0))
            {
                startPos = Input.mousePosition;
                deltaPos = new Vector3();
            }

            // マウス左ボタンドラッグの時
            if (Input.GetMouseButton(0)) {
                //タッチし始めた時の座標からどれだけ動いたかを計算
                deltaPos = Input.mousePosition - startPos;    
            } else {
                deltaPos *= dampingRatio; //マウスが押されていなかったら慣性スライド
            }
            startPos = Input.mousePosition;

        } else { //実機上で動作するコード

            if (Input.touchCount > 0)
            {
                Touch touch = Input.touches[0];

                if (touch.phase == TouchPhase.Moved) {
                    deltaPos = touch.deltaPosition;
                }

            } else {
                deltaPos *= dampingRatio; //指が離されていたら慣性スライド
            }
        }

        originX += deltaPos.x;

        if (originX != oldOriginX) //スライドしていたら以下を実行する
        {
            //必要以上に左へスライドしないようにする
            if (originX < centerX - (buttons.Length - 1) * distance)
            {
                originX = centerX - (buttons.Length - 1) * distance;
            }

            //必要以上に右へスライドしないようにする
            if (originX > centerX)
            {
                originX = centerX;
            }

            //全ボタンのサイズと位置を更新する
            UpdateSizesAndPositions();
            oldOriginX = originX;
        }
    }

    //originXに従って全ボタンの座標を更新する
    void UpdateSizesAndPositions ()
    {
        // まず最初に左端のボタンから位置とサイズを更新する
        float originDx = DistanceFromCenterX(originX);
        float originSize = SizeOfDistanceX(originDx);
        UpdateSizeAndPosition(buttons[0], originSize, originX);

        //左端のボタンを基準に、1つずつ全てのボタンの位置とサイズを更新する
        for (int i = 1; i < buttons.Length; i++)
        {
            // 自分の1つ左のボタンから横にdistanceだけ離れた場所に移動する
            float x = 
                buttons[i - 1].transform.localPosition.x + distance;

            // 移動先のx座標をもとに距離とサイズを求めて更新する
            float dx = DistanceFromCenterX(x);
            float size = SizeOfDistanceX(dx);
            UpdateSizeAndPosition(buttons[i], size, x);
        }

        // ボタンの配列をサイズの昇順にソートする
        Button[] sortedButtons = SortButtonsWithSize(buttons);

        // サイズの小さい方から、描画順が奥になるようにする
        for(int i = 0; i < sortedButtons.Length; i++)
        {
            sortedButtons[i].transform.SetAsLastSibling();
        }

    }

    // 与えられたボタンのx位置とサイズを更新する。
    void UpdateSizeAndPosition(Button button, float size, float x)
    {
        button.transform.localScale = new Vector3(
            size,
            size,
            1.0f
        );

        button.transform.localPosition = new Vector3(
            x,
            centerY,
            baseZ
        );
    }

    // 与えられたxが中心からどれだけ離れているかを計算する(絶対値)
    float DistanceFromCenterX(float x)
    {
        float dx = centerX - x;
        if (dx < 0.0f) {
            return dx * -1.0f;
        }
        return dx;
    }

    // 与えられた距離に応じてサイズを計算する(最小値はminSizeに固定)
    float SizeOfDistanceX(float dx)
    {
        float size = 
            maxSize - ((maxSize - minSize) / (backgroundWidth / 2.0f)) * dx;
        if (size < minSize) size = minSize;

        return size;
    }

    // ボタンの配列をサイズ順にソートする
    Button[] SortButtonsWithSize(Button[] sortingButtons)
    {
        // ソートの結果を保持するためにまず配列をまるごとコピーする
        Button[] sortedButtons = new Button[sortingButtons.Length];
        sortingButtons.CopyTo(sortedButtons, 0);

        // サイズでソートするためのComparerを指定してソート
        SizeXComparer comp = new SizeXComparer();
        Array.Sort(sortedButtons, comp);

        return sortedButtons;
    }
}
    
// サイズでソートするためのComparer
public class SizeXComparer : System.Collections.IComparer
{
    public int Compare(object a, object b)
    {
        //x方向のサイズを比較する
        float comp = ((Button)a).transform.localScale.x - ((Button)b).transform.localScale.y;
        return (comp < 0 ? -1 : 1);
    }
}

……いきなり全ソース書いて解析は丸投げって感じですみません……(><;)

ただ、そこまで難しいことしてないと思うんで、たぶん解析はそこまで難儀しないと思います。

一応、このソースを書くのに苦労というか、辛かった点をメモ程度に。

  • 「Input.touchCount」等のTouch関連のメソッドがエディター上では動作しないことを知らなかったので、最初なぜtouchCountがずっと0なのかで悩んでしまった。(そのことに気づくまで1時間ぐらい使ったと思う。……アホかわたしは……)
  • どういうロジックでボタンの位置とサイズを動的に変更するか悩んだ。
    →最終的には横位置を固定にしてサイズだけ中心からの距離に応じて変化するようにした。最初はボタン間の位置も動的に変更しようと思ったが、位置とサイズがどちらも同時に変化するとどこかで不整合が生じるので難しいと感じて諦めた。
  • 中心付近でボタンが重なりあってしまった時、描画順が意図した通りにならなかった。
    →最初、z軸で描画順をコントロールしようとしたがuGUIではうまくいかなかった。なので、sizeの大きさでボタンの配列をソートして、sizeの小さい順に「Transform.SetAsLastSibling()」を実行するようにした。結果的に、サイズの大きいものが描画順で手前に来るようになった。
  • 上の話とかぶるが、1フレーム内で何度もループを回しているのでなんとなく処理が冗長なような気がする……が、どこをどう修正すればループを減らせるかわからなかった。誰か教えて下さい…… ><;
  • C#で配列のソートを初めてやってみたが、果たしてこのやり方がスタンダードなものなのかどうかわからなかった。誰か教えて下さい…… ><;
  • 慣性スクロールを使おうと思って、すでにuGUIに似たような機能が搭載されてるんじゃないかと思って調べたが見つからず、けっきょく自前で実装してしまった。

4. SlideViewControllerの使い方

「1. とりあえずSlideViewControllerを作る」で作成した「SlideViewController」Panelに「3. SlideViewControllerスクリプトを作成する」で作成した「SlideViewController」スクリプトをくっつける。(Hierarchyビュー上にドラッグ&ドロップ)

それからInspectorビュー上で、各種設定値を設定する。
ちなみに冒頭のgifアニメを作る時に自分が設定した値は以下の通り。(事前に画面を横向きに設定してます)

  • Buttons : Sizeは18。「2. SlideViewControllerの下にボタンを並べる」で作ったボタンの個数に合わせる。そしてボタンを全て、この配列の要素に紐付ける。
  • CenterX : 0
  • CenterY : 0
  • BaseZ : 0
  • BackgroundWidth : 560(微調整していい塩梅を探してください)
  • MaxSize : 2.0(微調整していい塩梅を探してください)
  • MinSize : 0.5(微調整していい塩梅を探してください)
  • Distance : 120(微調整していい塩梅を探してください)
  • DampingRatio : 0.95(微調整していい塩梅を探してください)

とりあえずこの状態で、Playボタンを押すと、横にスクロールするボタンのリストが実現すると思います。
あとは通常どおり、このボタンにクリックイベントを結びつけるなり、ボタンに画像を設定するなりお好きなように。

5. 今後の課題

とりあえず今日はここまでで力尽きましたが、まだまだ改善の余地はあると思います。
ひとまず、以下に現状考えられる改善点をいくつか。

  • Panelの形に応じて、ButtonのSpriteをマスクするようにしたい。(Panelからはみ出たButtonは描画しない)
  • 遠くへスライドされたボタンは自動的にDestroyして、さらにこれから新たに表示されるであろうボタンを動的に作成して、無限スクロールを実現したい。(現状、左端のボタン位置を基準にその他のボタン全てを並べているので、このままのロジックではきびしい)
  • 1フレーム内でのループ数を減らしたい。
  • その他いろいろ……

今後、また改善を加えたりしたらブログにするかも知れません。
(……気が向いたら)

んでは、また近いうちに。

ガンズターンのRyosukeでした! m(_ _)m

Pocket

コメントを残す

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

トラックバックURL: http://gunsturn.com/2015/08/19/studying_unity_002/trackback/