C#
Unity3D
Unity
NGUI

NGUIをスクリプトのみで構築したいので調べてみた

More than 3 years have passed since last update.

はじめに

NGUI自体が初めてなのでDoxygenのリファレンス見ながら勉強した。
気が向いたときに追記とかしていきます。

NGUIの所感

全体的に優れた設計思想でAPIのネーミングも分かりやすいと思った。
ユーザも多く、メンテナもやる気があるようなので安定して使えそうな予感。

この記事のゴール

Hierarchyを使わずにGUIを構築する。

ゴールを目指す理由

Hierarchyを使って構築すること事態は悪いことではないが、同一シーンで複数の画面切替え等がある場合は相性が悪いと私は考えている。またデータを読込んで似たような画面をたくさん作るときとか。

あとDesignerとかでもあったように開発者の誤操作で気づかぬうちに大事なプロパティが変わってしまったり、そのことに気づけなかったり(テストコードを走らせれば防げそうだけど)することがあったので基本的に好きではない

ソースコードを見て変更が追えないって言うのもネックで、基本的にすべてをコードで書きたい派です。(設定ファイルがXAMLくらい可読性が高ければね...)

Unityの流儀からは外れていますけどね。

目標については極端なので、本当に全部スクリプトでGUIを構築すると工数が馬鹿にならないです。

実際は大まかな部分はEditorで作るべきで

  • 似たような画面/部品を大量に生成する場合
  • 仕様変更が頻繁に発生することが予想できるプロジェクト

等で項目や設定をテキストデータでリスト化して、それを読込んでスクリプトで自動生成するような場合に参考になるのではないでしょうか(大量のHierarchyを一個一個個別に修正するのは大変ですよね。漏れがあったら目も当てられませんし)

使った環境

  • Unity 4.5
  • NGUI 3.6.2

実践にあたって

だいたい必要なメソッドはNGUIToolsクラスで提供されているみたいです。
さすがにAtlas/Fontの生成とかはEditorでやった方が良さそう

UIツリーを構築する

UIRootを生成する

NGUIでUIを構築するためにはUIRootを生成する必要があるのでCreateUIメソッド生成する

UIPanel rootPanel = NGUITools.CreateUI(false);
GameObject rootObject = rootPanel.gameObject;
UIRoot uiRoot = rootObject.GetComponent("UIRoot") as UIRoot;

上記のコードで生成されたrootPanelがUIのRootになります。

カメラの取得

NGUIで使ってるカメラ等も取得しておくと便利かもしれません。
標準ではUIRootの直下にCameraという名前のオブジェクトに作られています。

GameObject cameraObject = GameObject.Find("/UI Root/Camera");
Camera camera = cameraObject.GetComponent<Camera>();
UICamera nguiCamera = cameraObject.GetComponent<UICamera>();

背景とかを変更する場合はCameraに背景色を設定します。

camera.backgroundColor = new Color(255,255,255,255);

Widgetを追加する

Ranelを作ったらそれにUIの部品(Widget)を追加しなければなりません。
これらはAddWidget<T>メソッドで追加出来ます。
Genericsな感じなのでUIWidgetのサブクラスなら何でも突っ込めるんじゃないでしょうかね。

UILabel myLabel = NGUITools.AddWidget<UILabel>(parentObject);

ただし追加したラベルは何もプロパティが設定されていないので
自分で設定してやる必要があります。

この辺は何らかのラッパーメソッドを作ってプロパティを設定出来るようにするとよいでしょう。

UILable AddLabel(GameObject go, string text)
{
    UILabel label = NGUITools.AddWidget<UILabel>(go);
    label.text = text;

    ...<中略>...

    return label;
}

Widgetを削除する

追加できたなら削除もしたい。
Destroyメソッドがそれに該当しそうです。

NGUITools.Destroy(myLabel.gameObject);

ただ、削除してくれるのはコンポーネントのみ。
たとえ単一コンポーネントでもオブジェクトそのものは削除されません。

UIPanelの追加

Panel等はWidgetのサブクラスではないのでAddChildで追加する必要があります。

UIPanel myPanel = NGUITools.AddChild<UIPanel>(parentObject);

各種Widgetの設定

AddWidgetで部品をPanelに追加した後はそれぞれ個別にpropertyを設定していく必要があります。

共通

Anchorの設定

UIRectから派生するクラスはAnchorPoint属性を持っています。
これを使うとレイアウトを他のWidgetやPanelの大きさの変化に合わせて
自身のサイズや位置を調整することができます

個人的にNGUIを使う上で最も大事な概念だと思います

UIRectクラスにはleft, right, bottom, topの4つのAnchorPointのインスタンスが生成されているので、
それらを上書きしていく形で設定します。

下の例は親オブジェクトparentに合わせてAnchorを設定するコードです
位置の計算にAnchorPointのrelativeとabsoluteが使われます。
それぞれの意味は次の通りです。

  • relative -- 水平方向時には左端を0.0f、右端を1.0f、垂直方向時には下端を0.0f、上端を1.0fとする相対的な位置を指定する。
  • absolute -- relativeで指定した位置から、設定されたpixel分だけ移動した位置を指定する。軸の向きに注意。
// parent is UIPanel
// label is UILabel

// parentの左端から5px右に移動した箇所をこのlabelの左端に設定する
label.leftAnchor = parent.transform;
label.leftAnchor.relative = 0.0f;
label.leftAnchor.absolute = 5;

// parentの右端から5px左に移動した箇所をこのlabelの右端に設定する
label.rightAnchor = parent.transform;
label.rightAnchor.relative = 1.0f; 
label.rightAnchor.absolute = -5;

// parentの上端から50px下に移動した箇所をこのlabelの下端に設定する
label.bottomAnchor = parent.transform;
label.bottomAnchor.relative = 1.0f;
label.bottomAnchor.absolute = -50;

 // parentの上端から5px下に移動した箇所をこのlabelの上端に設定する
label.topAnchor = parent.transform;
label.topAnchor.relative = 1.0f;
label.topAnchor.absolute = -5;

// 今設定したAnchorを反映させる
label.ResetAnchors();
label.UpdateAnchors();

Anchorを設定した後にResetAnchors()とUpdateAnchors()が必要みたいです。
SetAnchors(transform t)にもそのように書いてあります。

サブクラス毎の設定

UILabel

fontの設定

たぶん、プログラムで文字を表示することは多いのではないでしょうか
Fontに関しては事前にFontMakerでPrefabにしておくとよいと思います。
PrefabはResoucesディレクトリに入れておくとよいでしょう
以下はttfファイルから生成したダイナミックフォントなPrefabを読込んでLabelに設定するサンプルです

// labelはUILabelのインスタンスです
// fontPathはprefabへのpathです

GameObject fontPrefab = (GameObject)Resources.Load(fontPath);
GameObject fontObject = (GameObject)GameObject.Instantiate(fontPrefab);
UIFont font = fontObject.GetComponent<UIFont>();

label.bitmapFont = font;

UISpriteの設定

Widgetに画像を貼付ける場合はDraw Callの関係で複数のテクスチャを1枚にまとめたAtlasを使うと思います。
UISpriteはそのAtlasに格納されたSpliteを使って画像を描画するクラスです

Atlas, Spriteの設定

Atlasは事前にAtlasMakerで生成してPrefabにしておきます
以下はprefabから複製したAtlasからSpriteを設定するサンプルです

// spriteはUISpriteのインスタンスです
string atlasPath = "path/to/myatlas";
GameObject atlasPrefab = (GameObject)Resources.Load(atlasPath,typeof(GameObject));
GameObject atlasObject = (GameObject)GameObject.Instantiate(atlasPrefab);
UIAtals atlas = atlasObject.GetComponent<UIAtlas>();

sprite.atlas = atlas;
sprite.spriteName = "sprite_name";

Typeの設定

Typeの設定はUIBasicSpriteで実装されています。

Simple, Sliced, Tiled, Filled, Advanced値があり、初期値はSimpleになっています。
以下はTypeにTiledを指定するサンプルです。

// spriteはUISpriteのインスタンスです
sprite.type = UIBasicSprite.Type.Tiled;

UITextureの設定

UITextureはUnityネイティブのTextureを画像として表示するクラスです。
Resourcesから画像を読込んで表示することも出来ますが、
恐らくオフスクリーンレンダリングの結果の表示や、
なんらかの画像処理を施した結果等を表示するときに使われるのではないでしょうか。

画像の設定

以下はResourcesから画像を読込んで表示するサンプルです。

// textureはUITextureのインスタンスです
string texturePath = "path/to/mytexture";
Texture myTexture = (Texture)Resources.Load(texturePath);
texture.mainTexture = myTexture;

Buttonの作成

UIButtonUIWidgetのサブクラスではなくUIWidgetContainerのサブクラスらしい。ややこしい。
WidgetをButtonとして使うには、コンポーネントとしてButtonを追加する必要がある

WidgetにButtonを付与する

WidgetをButtonとして動かすために必要なコンポーネントはとUIButtonBoxColliderの2つです。
BoxColliderはボタンの当たり判定に使用されます。

以下はWidgetにButtonを付与するサンプルです。

// widgetはUIWidgetのサブクラスのインスタンス
widget.autoResizeBoxCollider = true;
// ↑ ColliderのサイズをWidgetサイズに合わせて自動調整するオプション

UIButton button = widget.gameObject.AddComponent<UIButton>();
BoxCollider collider = widget.gameObject.AddComponent<BoxCollder>();
collider.isTrigger = true;

Buttonの当たり判定をWidget全体に合わせる場合、autoResizeBoxCollderオプションをtrueにしておくとよいと思います。

Buttonにアクションを設定する

Buttonを生成したら、ボタンを押したときのアクションを設定しなければなりません。
アクションに設定できるCallBackメソッドの型は引数なし、戻り値無しのvoid()型です。

class MyWidget : UIWidget
{
    void OnClick()
    {
        ... // something to do
    }

    void CreateButton()
    {
        UIButton button = this.gameObject.AddComponent<UIButton>();
        // 中略
        EventDelegate.Add(button.onClick, OnClick);
    }
}

ButtonWidgetクラスの生成

Widgetは単体では画像も無いのでSpriteとLabelを併せ持つButtonWidgetクラスとか作っておくと便利かもね

using UnityEngine;

delegate void MyButtonWidgetOnClickHandler(MyButtonWidget buttonWidget);

class MyButtonWidget : UIWidget
{
    public event MyButtonWidgetOnClickHandler OnClickEvent;

    static public MyButtonWidgetSubClass Create<MyButtonWidgetSubClass>(GameObject parentObject)
        where MyButtonWidgetSubClass : MyButtonWidgetSubClass
    {
        MyButtonWidgetSubClass buttonWidget =
            NGUITools.AddWidget<MyButtonWidgetSubClass>(parentObject);
        buttonWidget.Init();
    }

    public UILabel Label{get;set;};
    public UISprite Sprite{get;set;};
    public UIButton Button{get;set;};
    public BoxCollider Collider{get;set;};

    virtual protected Init()
    {
        this.Label = NGUITools.AddWidget<UILabel>(this.gameObject);
        this.Label.SetAnchor(this.gameObject.transform);

        this.Sprite = NGUITools.AddWidget<UISprite>(this.gameObject);
        this.Sprite.autoResizeBoxCollider = true;
        this.Sprite.SetAnchor(this.gameObject.transform);

        this.Button = this.Sprite.gameObject.AddComponent<UIButton>();
        this.Collider = this.Sprite.gameObject<BoxCollider>();
        this.Collider.isTrigger = true;

        EventDelegate(this.Button.onClick, this.OnClick);
    }

    virtual protected void OnClick()
    {
        if(null == OnClickEvent){
            return;
        }
        OnClickEvent(this);
    }
}

最後に

追記していくときに大胆に構成とか変えたりしますがお察しください。
文章としては零稿未満です。編集履歴とか残ってるはずなので過去のバージョンも参照出来るはず…だよね。

間違いや、これはマズいという部分があればご指摘ください。

資料