読者です 読者をやめる 読者になる 読者になる

tonari note

オンラインゲームエンジニアの雑記

MarkUXとUniRXでGUIを作る話

この記事はUnity Advent Calendar 2015、23日目の記事です。

前日はkyusyukeigoさんのエディターでWebViewを使う話でした。
鯖蔵通信ゲームならサーバ周りをWebでポチポチしながら作業することは多いと思うので、夢が広がりんぐだと思います。

さて、今日はサーバ側ではなく、MarkUXとUniRxでクライアントのGUI実装を楽にしよう、という話です。

注意

この記事はuGUIを使用するのでUnity 4.6以降が必要です。
また以下のアセットを使用します。

また、スクリプトC#的な流儀にのっとっていません、あくまでサンプルとして短く書いています。
(MarkUXがプロパティに対応してないというのもありますが…)

はじめに

ある程度のゲームを開発するときに、必ずと言っていいほど必要になるものがGUIです。
GUIがなければユーザーが必要な情報を見れなかったり、必要な情報にアクセスしたりできなくなります。
それは当然Unityでゲームを作るうえでも必要になるものですが、ゲームの「面白さ」に直結する部分ではないので、GUIの実装に工数はあまり割きたくない、と思っているゲーム開発者は多いのではないでしょうか。

Unity4.6以降であれは、Unity標準のGUIシステム(uGUI)が使用できます。
しかし、完璧に使いやすいかといわれるとそうではありません。

そこに対する一つのアプローチがMarkUXになります。

uGUIの問題点

uGUIにおける最大の問題点となるのが、シーンやプレファブの管理でしょう。
バージョン管理上でコンフリクトしたとき、ForceTextオプションをつけていたとしても、マージは至難の業です。

Unityはエンジニア以外でも触れるツールで、個人的には様々な業種の方が使えるようになることが好ましいと思っていますが、やはりUnityに対する理解が少ないメンバーの操作が原因でGUIDが入れ変わって画像がmissingになったりすることもあるでしょう。

また、現状はHTMLにおけるCSSのようなものがなく、スタイルを分離することが難しい状態です。
たとえば木目調だったUIをプラスチック調にしたい、フォントをやっぱりこっちに変えたい、ということになると、様々なシーンに手を加えることになります。
そしてコンフリクトして(以下略。
ここでのプレファブ化は直接的な解決策ではないので、いずれ破たんし、地獄を見る羽目になります。

MarkUXでのGUI作成

まず、MarkUXに馴染みのない方も多いかと思うので、超簡単にMarkUXを紹介します。

Viewを作る

MarkUXではXMLを使用してUIを作成していきます。

<MyView>
  <Label Text="FUN for ALL, ALL for FUN."/>
</MyView>

と記述すると、

f:id:ykimisaki:20151207143206p:plain

こうなります。
ヒエラルキを見てみると、MyView配下にLabelが作成されています。
LabelはuGUIのTextコンポーネントに、MarkUXのLabelコンポーネントが張り付いたものです。
これはXMLから自動で生成(もちろん再生開始時にも再生成)されるので、Unityのシーンに対してできる限り非依存ということになります。

<MyView>
  <Label Text="デフォルト位置、デフォルトサイズ" />
  <Label Text="位置は上部、横幅はフォントに合わせる" Alignment="Top" AdjustToText="Width" />
</MyView>

このようにパラメータを続けて書いていくと、

f:id:ykimisaki:20151207144749p:plain

のように位置やサイズなどを指定することもできます。

f:id:ykimisaki:20151207150258p:plain

真ん中の文字が「デフォルト位」で止まっているのは、横幅が確保されていないためです。

値をスクリプトから入れる

せっかくViewを作っても、スクリプト側から値を入れられないのであれば話になりません。
MarkUXの場合、スクリプトのフィールドの値をそのまま表示させることができます。

<MyView>
  <Label Text="{SampleText}"/>
</MyView>
public class MyView : MarkUX.View
{
    public string SampleText = "さんぷる";
}

このように、Viewと同名のクラスを作成し、MarkUX.Viewを継承します。
そして、内部にあるpublicフィールド名を中括弧で囲み、XML側に記述します。
(残念ながらプロパティには対応しておらず、プロパティ対応も試してみたのですが複雑すぎて挫折しました。)

また、スクリプトでLabelを直接扱いたい場合は、以下のようにIdを指定します。

<MyView>
  <Label Id="Label1" />
</MyView>
public class MyView : MarkUX.View
{
    public MarkUX.Views.Label Label1;
}

これでスクリプトのLabel1フィールドに、View側のインスタンスが渡されます。

スタイルを使用する

スタイルは先ほどのView用のXMLとは別に、Theme用のXMLを使用します。
例えば、文字サイズを統一したい場合は、

<Theme Name="MyViewTheme">
  <Label AdjustToText="Width" />
  <Label Style="Big" FontSize="28" />
  <Label Style="Small" FontSize="12" />
</Theme>

のようにテーマを記述し、

<MyView>
  <Label Text="FUN for ALL, ALL for FUN." Style="Big" Offset="0,0,0,20px" />
  <Label Text="FUN for ALL, ALL for FUN." Style="Small" Offset="0,20px,0,0"/>
</MyView> 

のように指定すると、

f:id:ykimisaki:20151207145806p:plain

となります。
当然ですが、スタイル側を変更すれば、すべてのViewに反映されます。便利。

アニメーションさせる

GUIに求められるアニメーションは、移動するだけ、サイズを変えるだけ、透明度を変更するだけ等の簡素なもので十分ですが、GUIを構成するパーツは膨大なので、これらをすべての要素に対して1つずつ適応するのは骨が折れます。
コンポーネントを作成し貼り付けるにだけでもしんどい作業で、コンポーネントを貼る=シーン/プレファブに依存するということにもなります。

MarkUXではこれらの簡易なアニメーションを定義できます。

View側XML

<Theme Name="MyViewTheme">
  <ViewAnimation Id="FadeIn">
    <Animate Field="Alpha" From="0" To="1" Duration="0.2s" EasingFunction="QuadraticEaseIn" />
  </ViewAnimation>
</Theme>

これらの発火タイミングをスクリプト側で制御したいので、Idで渡してあげます。

public class MyView : MarkUx.View
{
    public MarkUX.Views.ViewAnimation FadeIn;

    public void OnHoge()
    {
        FadeIn.StartAnimation();
    }
}

またButtonやViewChanger(タブ操作でViewが切り替わる系)などでは特定のアニメーションをXML上に記述することも可能です。

MarkUXとUniRxをつなげる

さて、実際のゲームではUIに表示される値は随時変わっていきます。
そこで、UniRxを使用して値の変化を検知し、スクリプト上からLabelの値を随時変えていこうと思います。

<MyView>
  <Label Id="Label1" Text="Before" />
</MyView>
public class MyView : MarkUX.View
{
    public MarkUX.Views.Label Label1;
}

まず、Labelへの参照をスクリプトに持ちます。
そしてスクリプト側で

public class MyView : MarkUX.View
{
    public MarkUX.Views.Label Label1;

    public void Start()
    {
        Label1.SetValue(() => Label1.Text, "After");
    }
}

MarkUXのコンポーネントにはSetValueというメソッドが生えており、そいつを使うと上記のような感じで更新することができます。
これをUniRxと結び付けると、

public class MyView : View
{
    public Label Label1;
        
    // 外部から値を放り込まれる
    public ReactiveProperty<string> Text = new ReactiveProperty<string>();

    public void Start()
    {
        Text
            .Subscribe(x => Label1.SetValue(() => Label1.Text, x))
            .AddTo(this);
    }
}

これでUniRxでコネコネした結果をダイレクトにMarkUX上のコンポーネントに反映させることができます。
ただ、これだと使いづらいので、UniRxにあるSubscribeToTextのような拡張メソッドを用意してあげます。

public static class MarkUXObservableExtensions
{
    public static IDisposable SubscribeToLabel<T>(this IObservable<T> source, Label target)
    {
        return source.Subscribe(x => target.SetValue(() => target.Text, x));
    }
}

これで先ほどのスクリプト

public class MyView : View
{
    public Label Label1;
        
    // 外部から値を放り込まれる
    public ReactiveProperty<string> Text { get; private set; }

    public void Start()
    {
        Text.SubscribeToLabel(Label1).AddTo(this);
    }
}

アラステキ!
という便利メソッドをたくさん作るとモアモアベターですね。
(個人で作った拡張メソッド群はそのうちGistにあげようかなと思っています。)

XAMLだと、ReactivePropertyのValueに対して直接バインドができてハピハピなんですが、MarkUXはそこまで高機能ではないっぽいです。
実際もろもろ魔改造してプロパティに対応させようとしていたのですが、中身がリフレクション芸すぎて色々と参ってやめました…
人間、すなおが一番ですね。

さいごに

さて、MarkUXとUniRxでGUIを作ってきました。
MarkUXはメチャイケだから使え!と言っているのではなく、当然開発スタイルに合わせてuGUIをそのまま使っても、NGUIを使っても全く問題ないです。

しかし、

  • ロジック部分はUIとは無関係にする
  • ロジックからUI、UIからロジックへの流れを作る
  • 統一感のあるUIを作るためにスタイルを有効に使う
  • Unityのシーン/プレファブにできる限り非依存にする

ということをやりやすいのが、MarkUXとUniRxの合わせ技の利点といえるので、これらを求めている人に対しては実用的なアセットではないでしょうか。
また、スマフォ向けパズルゲームのパズル操作部分のUIなんかはよりパフォーマンスが求められ、そういう部分にはMarkUXは向いていないといえるので、きっちりと使用すべき場所を把握しておくのも大事かと思います。

みなさんも是非、MarkUXとUniRxで楽しいuGUIライフを!

おしらせ

さて、話は別になりますが、実は冬コミに参加します。

3日目 西 え-07a です。
白鳥トモエ(Tokyo 7th Sisters)と宮本フレデリカ(THE IDOLM@STER CINDERELLA GIRLS)のイラスト本でお待ちしております。

次はrodostwさんです。