ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

Unity の Android ビルドを CLI からおこなう

現在開いている Unity をいったん終了しないと実行できないので、
あんまり Unity の CLI は使い勝手よくないんですが、ハマりどころもあったので、いったんメモとして。

1. ビルドのために実行するコマンド

公式ドキュメントでは、 macOS の場合 /Applications/Unity/Unity.app/Contents/MacOS/Unity に Unity コマンドがあることになっていますが、
私は Unity Hub でインストールしているため、
/Applications/Unity/Hub/Editor 以下に Unity.app があります。

よって今回は、以下のような実行コマンドとなりました。

/Applications/Unity/Hub/Editor/2019.1.3f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -executeMethod BuildBatch.DebugBuildAndroid

Unity のバージョンアップのたびに Unity コマンドの位置が変わるのか……と考えると、
Unity Hub を使うのはなんとも微妙な気持ちになります……。

それはさておき、 -executeMethod BuildBatch.DebugBuildAndroid が大事なところで、
Android ビルドの場合、面倒ですがスクリプトを用意してあげる必要があります。
次の項でそれを紹介します。

2. ビルド用スクリプトの用意

今回は以下のようなスクリプトを、 Assets ディレクトリの下に Editor という名前のディレクトリを作り、その中に置きました。
この Editor という名前が大事なポイントで、
Unity は Assets ディレクトリ下にある Editor ディレクトリを特殊扱いします。
(参考: https://docs.unity3d.com/ja/current/Manual/SpecialFolders.html

もし仮に以下のスクリプトを Editor ディレクトリ下に置かないと、

The type or namespace name 'Build' does not exist in the namespace 'UnityEditor' (are you missing an assembly reference?)

と言われビルドエラーになってしまいます。

「Editor ディレクトリを作りたくない! 横着したい!」という場合は、
スクリプトの最初の行に #if UNITY_EDITOR 、最後の行に #endif と書くことで、
ビルド時にコンパイル対象とならないので回避できます。

話を戻しまして、以下のようにスクリプトを書きました。

using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEngine;

public class BuildBatch {
    static void DebugBuildAndroid() {
        BuildSummary summary = BuildAndroid();
        if (summary.result == BuildResult.Failed) {
            EditorApplication.Exit(1);
        }
    }

    [MenuItem("File/Build Android", false, 220)]
    static void DebugBuildAndroidMenuItem() {
        BuildTargetGroup currentBuildTargetGroup = EditorUserBuildSettings.selectedBuildTargetGroup;
        BuildTarget currentBuildTarget = EditorUserBuildSettings.activeBuildTarget;

        BuildSummary summary = BuildAndroid();

        EditorUserBuildSettings.SwitchActiveBuildTargetAsync(currentBuildTargetGroup, currentBuildTarget);
    }

    static BuildSummary BuildAndroid() {
        BuildPlayerOptions options = new BuildPlayerOptions();
        options.target = BuildTarget.Android;

        string[] scenes = new string[EditorBuildSettings.scenes.Length];
        for (int i = 0; i < scenes.Length; ++i) {
            scenes[i] = EditorBuildSettings.scenes[i].path;
        }
        options.scenes = scenes;

        string assetsDir = Application.dataPath;
        options.locationPathName = $"{assetsDir}/../Build/{Application.productName}.apk";

        options.options = BuildOptions.Development;

        // https://docs.unity3d.com/ja/current/ScriptReference/BuildPipeline.BuildPlayer.html
        BuildReport report = BuildPipeline.BuildPlayer(options);
        BuildSummary summary = report.summary;

        if (summary.result == BuildResult.Succeeded) {
            Debug.Log("Build succeeded: " + summary.totalSize + " bytes");
        }

        if (summary.result == BuildResult.Failed) {
            Debug.LogError("Build failed");
        }

        return summary;
    }
}

基本的にはこちらの記事を参考にしつつ、
Unity 2018.1 からは BuildPipeline.BuildPlayer が string 型でなく BuildReport クラスを返すので、
そこを変更したり一般化させたりしています。

今回はトップディレクトリに作った Build ディレクトリに apk ファイルを書き出すようにしています。(なお、同名ファイルがすでに存在する場合は上書きされます)

また、開発用ビルドとなるように options.options = BuildOptions.Development; と指定しています。
(参考: https://docs.unity3d.com/ja/current/ScriptReference/BuildOptions.html
BuildOptions を複数指定したい場合にどう書けばいいかはまだわかっていません……。

私としては Unity を閉じなくても Android ビルドをしたかったので、
DebugBuildAndroidMenuItem というメソッドを足していますが、不要な方は削除してください。

f:id:nekonenene:20190522183038p:plain

3. まとめ

上記のスクリプトを用意することで、 BuildBatch クラスの DebugBuildAndroid メソッドが存在することになったので、
-executeMethod BuildBatch.DebugBuildAndroid の指定が可能になります。

現在起動中の Unity がある場合はそれを閉じたあと、 1. で書いた

/Applications/Unity/Hub/Editor/2019.1.3f1/Unity.app/Contents/MacOS/Unity -quit -batchmode -nographics -executeMethod BuildBatch.DebugBuildAndroid

を実行することで、 apk ファイルを出力してくれます。

うーん、面倒ですね……。
CI でビルドをおこなえるようにすれば意義が出てくるんでしょうけど、
まだそこまで作り込んではいないので、この状態では旨味が薄いです。

以前書いたこの記事と組み合わせて、CircleCI による Android ビルドをおこないたいですねー。

復習としては

  • Android ビルドの場合はスクリプトを書く
  • スクリプトは Assets/Editor ディレクトリ下に置く
  • DebugBuildAndroid メソッドを用意したのち Unity -quit -batchmode -nographics -executeMethod BuildBatch.DebugBuildAndroid で実行(※ Unity コマンドの場所は要確認!)

というのがポイントでした。

せっかくここまで書いたなら、CI でビルド回して deployagate に飛ばしたいですね!
がんばります!

【追記】
CircleCI でのビルド、Docker 内に Unity と Android SDK を入れた上で Android ビルドに成功させるのがそうとう困難でしたので、手動運用か、
もしくは Unity, Android SDK, Jenkins を入れたパソコンを用意して自宅サーバー化し、
Jenkins の Unity3dBuilder Plugin を利用してビルドしてあげるのが良さそうです……

Unity の uGUI でウィンドウのフェードアウト処理

ゲームを作るなら細かい演出は大事! フェードアウトは大事!

Unity で、GameObject のフェードアウト処理をおこないたくてネットで探したんですが、
GameObject 単体のフェードアウト処理になっているものが多かったので、
All Children GameObject (すべての子オブジェクト)に対しても同時におこなえるフェードアウト処理をおこなう方法について書きます!

なお、シーン間のフェードアウト・イン処理をおこなう方法としては、
こちらの 『Simple Fade Scene Transition System』 というアセットがおすすめです。
今回のコードを書くにあたって参考にした部分が多いです。

ポイント1: CanvasGroup

こちらの Q&A が参考になりました。 : How to fade a game object will all its children?

GameObject の Image に対して処理をおこなう方法ですと、
子オブジェクトのアルファ値はそのままになってしまいます。

Canvas Group を目的の GameObject にコンポーネントとして付与することで、
Children GameObject を含めたアルファ値の操作を楽におこなうことが出来ます。

f:id:nekonenene:20190519180615p:plain
Canvas Group Component は Alpha を値として持つ

ポイント2: EventSystem

また、フェードアウト中はユーザーの操作を受け付けたくありません。

タッチ操作やクリック操作を無効にする必要があります。
そのためには、操作の受け付け担当である EventSystem さんを一時的に無効にしてあげるのが一番です。

EventSystem eventSystem = GameObject.FindObjectOfType<EventSystem>();
eventSystem.enabled = false;

でおこなえます。true に戻してあげるのを忘れないよう注意しましょう!

(参考: UnityのEventSystemを操る - あさちゅんのゲームブログ

そして出来上がったコード

出来上がったコードがこちらです。

対象の GameObject に、独自 Component である Fadeout Component と、前述した CanvasGroup Component を一時的にアタッチし、
フェードアウトが終わったら削除し、 GameObject に対して SetActive(false) を実行しています。

Destroy(this); のあとのコードが実行されるのが自分的には不思議なんですが、ここなんでなのでしょう……わかる方いらっしゃいましたら教えてください)

なお、独自 Component を作ってアタッチしているのは、 StartCoroutine メソッドなどの実行に Context (現在の状態)を渡す必要があるからです。
(参考: coroutineとasync/awaitのあれこれ
もちろん、 new GameObject(); で GameObject を作って、そこに Fadeout Component をアタッチするという方法でも可能です。

呼び出し側のコード

呼び出し側は以下のようなコードです。
ブログ用にいろいろ省略していますが、 OnClickCloseButton メソッドを Button の OnClick 用のメソッドとして使い、
OpenSettingsWindow メソッドや CloseSettingsWindow は別クラスから呼び出されることもあります。

using UnityEngine;
using Util;

public class SettingsWindowController : MonoBehaviour {
    GameObject settingsWindowPanel;

    void Start() {
        settingsWindowPanel = this.gameObject;
        settingsWindowPanel.SetActive(false);
    }

    public void OpenSettingsWindow() {
        settingsWindowPanel.SetActive(true);
    }

    public void OnClickCloseButton() {
        CloseSettingsWindow();
    }

    public void CloseSettingsWindow() {
        Fade.FadeoutObject(settingsWindowPanel, 0.3f);
    }
}

できあがり

無事に、ウィンドウやその中のテキストなどがいっしょにフェードアウトしてくれました!

f:id:nekonenene:20190519184434g:plain
Fade out the window on close

きっと似たようなコードでフェードイン処理も作れる気がするので、
次はフェードイン処理も作っておきたいな、と思いました。

この記事が Unity を使っている方のお役に立てましたら幸いです。

materialize-css のJSライブラリを TypeScript + React 環境で読み込む

Materialize 、モダンっぽい Web サイトを作れるので、
個人的には Bootstrap よりは最近使うようになりつつあります。

「Bootstrap より軽いし……」と書こうとして実際にいま測ってみたらそうでもなかったですが(笑)

1. Bootstrap vs. Materialize (ファイルサイズ比較)

1-1. Bootstrap

  • bootstrap.min.js (v4.1.3) : 20.56KB
  • bootstrap.min.js (v4.1.3) : 13.76KB
  • jquery-3.3.1.slim.min.js : 23.47KB
  • popper.min.js (v1.14.3) : 6.8KB

Total: 64.59KB

1-2. Materialize

Total: 59.53KB

Materialize の JavaScriptjQuery を内包しているので、そのぶん大きくなっていますね。
読み込むファイル数は少ないので、そのぶんコストは低いですが、思ったより容量に差はなかったです。

閑話休題。本題にいきましょう。

2. Materialize の JavaScript ライブラリを TypeScript で使いたい

例えば materialize-cssNavbar Component の Dropdown を使いたいときには、
Dropdown のドキュメントにある通り、

// ドキュメント通りに dropdown-trigger class を設定し、以下の script を末尾などに記述
document.addEventListener('DOMContentLoaded', function() {
  var elems = document.querySelectorAll('.dropdown-trigger');
  M.Dropdown.init(elems, {});
});

// jQuery を呼び出している場合は以下の 1 行で済む
$('.dropdown-trigger').dropdown();

上のどちらかの方法でドロップダウンをアクティブにしなくてはいけません。

HTML の最後に script タグを使って、

<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    var elems = document.querySelectorAll('.dropdown-trigger');
    M.Dropdown.init(elems, options);
  });
</script>

こんな感じで書いてあげてもいいわけですが、
せっかくなら ts ファイルや tsx ファイル内で呼び出してあげたい。その方法です。

3. 答え

3-1. npm install -D @types/materialize-css

前置きが長くなりましたが、以下のようにライブラリをダウンロードすればOKです。

npm i -S materialize-css
npm i -D @types/materialize-css

materialize-css だけでなく、
@types/materialize-css を入れているのが一番のポイントです。
これがないと TypeScript でのコンパイルに失敗してしまいます。

なお、古い記事だと npm i -S materialize-css@next で入れている記事がありますが、
2019年5月現在では、 npm i -S materialize-css で入るのは 1.0.0 、
npm i -S materialize-css@next で入るのは 1.0.0-rc.2 と、
next を付けるほうがむしろ古いバージョンが入ってしまうのでお気をつけください。

f:id:nekonenene:20190506005825p:plain
https://www.npmjs.com/package/materialize-css?activeTab=versions より

3-2. import M from 'materialize-css'

あとはいつも通りに、 tsx ファイルや ts ファイルの先頭で import してあげれば OK です。

import M from 'materialize-css';

2. の項で書いた例のように、

var elems = document.querySelectorAll('.dropdown-trigger');
M.Dropdown.init(elems, {});

を適当な場所で書くことにより Dropdown の初期化をおこなうこともできますが、
ドキュメントの例にあるようなクラス名(dropdown-trigger)をそのまま使う場合でしたら、

M.AutoInit();

でカンタンに済ませることも可能です。(参考: https://materializecss.com/auto-init.html

どのコンポーネント、そして class 名だと初期化されるかは元のコードを読むといいでしょう。

https://github.com/Dogfalo/materialize/blob/efa0aee/js/global.js#L106-L135

3-3. React で扱う場合の Tips

M.AutoInit();componentDidMount 内で呼び出すと良いでしょう。
(※ constructor 内で呼び出すと失敗します。constructor は HTML render がおこなわれる前に走るためです)

public componentDidMount(): void {
  M.AutoInit();
}

M.AutoInit(); は便利ですが、すべてのコンポーネントに関する init がおこなわれるわけではありません。
例えば M.updateTextFields(); もその一つです。(参考: 『Text Inputs』 の『Prefilling Text Inputs』の項)
これは、Text Input Form にすでに文字が入力されている場合には、ラベルを上に移動しておいてくれるメソッドです。

https://github.com/Dogfalo/materialize/blob/efa0aee/js/forms.js#L3-L21

使用しないとこうなる:

f:id:nekonenene:20190506021047p:plain

使用するとページ表示時にこうなってくれる:

f:id:nekonenene:20190506021123p:plain

これは M.AutoInit(); から呼び出されないことがコードからわかりますので
入力フォームのあるページでは M.updateTextFields(); を使ってあげるといいです。

私の書いているコードでは、input フォームの defaultValue を非同期通信で取ってくるために
render のされ直しが発生してしまうため、
componentDidMount 内ではなく componentDidUpdate 内で呼び出してあげています。

public componentDidUpdate(): void {
  M.updateTextFields();
}

4. おしまい

React を今まで避けて生きてきたんですが、
かんたんな Go (Gin) + React のアプリケーションを作ってみようとさわってみたら
ちょっとずつわかってきました。

Redux には触れていないですが、今のところバケツリレーも大規模になっていないので、
今回は使わないでもいけそうです。

ついでに TypeScript も導入し、数年ぶりにさわっていますが、
慣れていない Go + 慣れていない React + 慣れていない TypeScript の組み合わせでちょこちょこ苦しんでいます(笑)
型チェックのために書く量は増えるし、参考資料そのままで書けない場合もあり大変です。

とても勉強になっているので、もう少しがんばってみます!