inosyanのブログ

プログラム、家庭菜園、僕が興味を持ったことを書いていきます

昔作った Adobe AIR のアプリをメンテしようとしたら、リリースビルドが出来ずに苦労した件

Flash Builder x AIR

f:id:inosyan:20180218104818p:plain

Adobe AIR は、一つのソースコードから様々なOSやデバイス向けのアプリを作れる仕組みです。
そして、Flash Builder は、Adobeが開発したデベロッパー向けのアプリで、FlashAIRの開発が行えます。Flashで開発してた頃はメインで使っていました。数年前まではAIRを使って、デスクトップアプリ向けやモバイル向けに、いろんなゲームやツールを作ってました。

昔作ったairアプリがインストールできなくなってしまった

ふと、昔作ったアプリのインストーラーである、拡張子 .air ファイルをクリックすると、

Sorry, an error has occurred.
The application could not be instlled because the installer file is damaged. Try obtaining a new installer file from the application author.

というエラーが出ました。

f:id:inosyan:20180218104851p:plain

ネットの情報では、このエラーはインストーラーを作ったときの証明書が古いと出るそうで、たしかにインストーラーをコンパイルしたのは今から数年前で、その証 明書を作ったのはそれから更に数年前なので、かなり古いです。

再ビルドしようとしたらエラーが出た

それなら新しく証明書を作りビルドしなおせば良いと思い、そうしようとしたところ、こんどは、

Error creating AIR file:Could not generate timestamp: Operation time out

というエラーが出ました。

f:id:inosyan:20180218104909p:plain

調べて見ると、これは、Flexの使っている Java 6 が参照しているタイムスタンプサーバがいまは動いてないのが問題で、Java 7 か 8 に上げれば解決するそうです。
Fixing the ‘timestamp’ error when packaging an AIR app for desktop

Javaのバージョンをあげる

マシンにはすでに Java 8 が入っています。ところが、Flash Builderは古いバージョンのJavaを参照しているので、新しい方を参照するように直す必要があります。 macでの対処法はここに書いてあったので、このとおりにやってみました。 Running Adobe Flash Builder on Win and Mac with Java 7/8 (UPDATED)

まず、ターミナルで、Javaのバージョンを調べます。

java -version

私の環境では、このように返ってきました。

java version "1.8.0_71"

このバージョンをメモしておき、次のコマンドのJavaのバージョンjdk1.8.0_71のところは、自分の環境のものに置き換えて実行します。

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home

次のコマンドはroot権限が必要です。これもJavaのバージョンを自分の環境のものに置き換えて実行します。これは、私の環境では失敗しましたが、このパスにフォルダをつくればいいのでFinderから手動で作っても大丈夫です。

sudo mkdir –p /Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/bundle/Libraries

続けてこのコマンドを実行します。これもJavaのバージョンを自分の環境のものに置き換えて実行します。

sudo ln -s /Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/jre/lib/server/libjvm.dylib /Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/bundle/Libraries/libserver.dylib

上記JAVA_HOMEに設定したパスの一つ上に Info.plistというファイルがありますが、これをテキストエディタで開くと

<key>JVMCapabilities<key>
<array>
    <string>CommandLine</string>
</array>

の箇所があるので、array の中に2行追加します。

<key>JVMCapabilities<key>
<array>
    <string>JNI</string>
    <string>undledApp</string>
    <string>CommandLine</string>
</array>

そして、Flash Builderのアプリ "/Applications/Adobe Flash Builder 4.7/Adobe Flash Builder 4.7.app" のところまで行き、右クリックでコンテキストメニューを開き、Show Package Contents で中身を見ます。更に奥の階層の中の、Contents/MacOS/Adobe Flash Builder 4.7.ini をテキストエディタで開きます。このファイルまでのフルパスは、
/Applications/Adobe Flash Builder 4.7/Adobe Flash Builder 4.7.app/Contents/MacOS/Adobe Flash Builder 4.7.ini
です。 そして、このファイルの最後の行に、

-Djava.library.path=../../../eclipse/plugins/com.adobe.flexide.nativelibs_4.7.0.349722/os/macosx/:../../../eclipse/plugins/com.adobe.flexide.amt_4.7.0.349722/os/macosx/:../../../eclipse/plugins/com.adobe.flexbuilder.utils.osnative_4.7.0.349722/os/macosx/

を足します。念のため事前にこの相対パスが存在するか確かめてみましたが、私の環境では大丈夫でした。

そして、これが大事ですが、マシンを再起動 してください。Flash Builderを再起動しただけでは変更が反映されません。

最新のAIR SDK を昔のやり方で入れようとして失敗

実は、Flash Builder を Java 8 で動くようにしただけでは、Timestampの問題は解決しませんでした。調べてみると、SDKを最新にしたら直ったという報告があったので、最新を入れてみることにしました。

ですが、AdobeSDKは4.6が最後のバージョンで、その後オープンソース化されて、いまは Apache Flex SDK という名称に変わっていました。 ちなみに、SDKAdobeが開発していた時代は FlexAIR は別々に配布されていて、zipを解凍したものを、Flexのフォルダの中にAIRのフォルダの中に混ぜて使っていました。混ぜたフォルダをアプリの sdks フォルダの中に置き、Flash Builderの プロジェクトのプロパティの Flex Compiler から Configure Flex SDKs を選んで、そのフォルダを読み込めば利用できます。

まず、今回の問題はAIR書き出し時の問題なので、最新の AIR SDK 28.0 をFlex4.6に混ぜてみました。しかし、Timestampの問題は解決しません。

AIR SDKのバージョンがあがったので、アプリ名-app.xml名前空間

<application xmlns="http://ns.adobe.com/air/application/3.1">

のバージョンの部分を

<application xmlns="http://ns.adobe.com/air/application/28.0">

に変えました。しかし、今度は、その名前空間が認識できないというエラーが出て、ビルドできません。

最新のFlex SDK を昔のやり方で入れようとして失敗

Flex SDKも最新にしないといけないことに気づき、sdkをzipで落としてきて入れようとしたら、Flash Builderがそれをうまく読み込んでくれませんでした。エラーメッセージを見るといろんなファイルが足りないようです。おそらく著作権などの問題が絡んでいて一緒に配布できないのでしょう。一つずつ入れて行こうとしましたが、 ant を使って解決できると書いてあったので試してみました。ですが、うまくいきませんでした。

SDK Installer を使ったらSDKを最新にできた

いろいろ探していたら、SDK Installer を見つけました。なんと今は、インストーラーを使ってSDKをインストールできるようになっていました。

f:id:inosyan:20180218104930p:plain

これを使って、SDKを最新にすることができました。

でもFlash Builderが不安定、パッチをあてて解決

その後、ビルドしようとすると、

Error: Comparison method violates its general contract!

というエラーが頻発し、うまくビルドできませんでした。 解決法を探していたら、 この記事 Error: Comparison method violates its general contract! の対処法 を見つけました。Flex SDKのバグのようで、その対処のパッチを作ってくれていたので、それを使わせていただいたところ、見事解決しました。

そして、無事、airアプリをビルドすることができるようになりました。

まとめ

今回はかなり大変でしたが、まとめると、Timestamp問題を解決するためには、これらが必要でした。

  • Flash BuilderをJava 8で動くように設定する
  • ApacheSDK Installerを使って FlexAIRSDKを最新にする
  • アプリ名-app.xml名前空間を最新のAIRのバージョンに書き換える
  • Flex SDKのバグに対処するためのパッチをあてる

Flashはもう2020年に終了が決まっていますが、AIRはどうもその後もまだ続くようです。 Flash Playerのサポートは2020年末で終了、ただしAdobe AIRは継続の方針

せっかく最新の環境にできたので、もうすこしやってみようかなと思いました。

複数のUnityのバージョンを管理できる「Unity Hub」

f:id:inosyan:20180209000726p:plain

Unityのベータ版のダウンロードサイトを見ると、ベータ版のインストーラーの隣に「Unity Hub」というものが並んでいました。

f:id:inosyan:20180209000757p:plain

スクリーンショットは Unity 2018.1.0b5 ですが、2/8の時点では Unity 2018.1.0b6 です。

説明にはこうあります。

Now available in Beta, Unity Hub simplifies how you find, download, and manage your Unity Editor installs and view all your Unity projects. It will also help you discover features to help you get started faster - such as the new Templates feature.

便利そうなので、早速インストールしてみました。

インストールすると、こんな画面が開きます。

f:id:inosyan:20180209000811p:plain

インストールしたばかりの時はまだ何も入ってないので、No versions available now と表示されています。

Beta Releases を選ぶと、ベータ版がダウンロードできます。

f:id:inosyan:20180209000830p:plain

ダウンロードしたバージョンは、On my machine に追加されます。

f:id:inosyan:20180209000845p:plain

左のメニューから Official Releases を選ぶと、安定版の
- Unity 2017.3.1f1
- Unity 2017.2.1f1
- Unity 2017.1.3f1

がリストアップされ、それぞれダウンロードできます。それ以前のバージョンはここには出てきませんが、On my machine の画面の 「+Locate a Version」 ボタンで、既にインストール済みのバージョンをリストに加えることができます。

f:id:inosyan:20180209000901p:plain

さらに、リストアップされたアプリ名の右の…をクリックすると出て来るポップアップメニューで、デフォルトで起動するバージョンを選択できるようです。

f:id:inosyan:20180209000916p:plain

上部メニューの Projects で、最近開いたプロジェクトの一覧が見れます。プロジェクトのパスの横には、そのプロジェクトを編集したUnityのバージョンが表示されています。
いままでは、まず適切なバージョンのUnityを起動しておいてからプロジェクトを開く必要がありましたが、Unity Hubからプロジェクトを選べば、適切なバージョンのUnityで開いてくれます。
また、プロジェクト名の右の…で開くポップアップメニューで、どのバージョンで開くのかを選択できます。

f:id:inosyan:20180209000937p:plain

新しいバージョンは通知欄からインストールできます。

f:id:inosyan:20180209000952p:plain

古いバージョンで作られたプロジェクトを保守したり、バージョンの違う複数のプロジェクトを同時に開発する際には便利ですし、気軽にベータ版を試せるのは嬉しいです。

アセットストア向けの開発は、最終成果物のアップロードを行なったバージョンがサポート対象バージョンになるため、できるだけ古いバージョンのUnityで動作確認するのですが、複数バージョンを使い分けできるUnity Hubは重宝します。これからはデフォルトで使うことになりそうです。

Unityの通信コードを自動生成する「WWW Script Generator」

f:id:inosyan:20180202233332p:plain
3週間ほど前に申請したUnityのアセットパッケージ「WWW Script Generator」がやっとUnityアセットストアにリリースされました。このアセットパッケージは、サーバー通信に関するスクリプトを自動で書いてくれるツールです。

Unityでサーバー通信する際は標準で用意されているWWWクラスを使います。そのまま使ってもいいのですが、たいていの場合は共通処理があるので通信用のクラスを作り、その中でヘッダーに何かを入れたり、戻り値をパースしたりすると思います。
また、サーバー側で用意されているAPIやパラメーター、戻り値の構造をそのまま文字列で表現するのはタイプミスによりバグを引き起こします。なので、それらをクラス、メソッド、プロパティで表現することが必要になりますが、APIの数が多くなるとそれを書くのも大変です。
それに、たいていの場合はサーバーとUnityのエンジニアは別の人が担当すると思いますが、仕様についてのコミュニケーションが多く発生し、伝達ミスも起きる可能性があります。仕様が固まってない段階で開発が始まることも多く、頻繁に仕様変更が発生し、そのたびにUnityの開発者は変更になった箇所を把握しつつスクリプトを修正しなければなりません。さらに通信テストにも多くの工数を必要とします。
f:id:inosyan:20180202233559p:plain

このパッケージを使えば、仕様を設定ファイルにしてサーバー開発者と共有することで、仕様の伝達ミスをなくすことができます。
また、スクリプトの自動生成機能により、スクリプト作成にかかる時間を大幅に短縮できます。
仕様が変更になった場合でも、変更箇所を明確に把握しスクリプトを再生成することで簡単に対応できます。
そして、スクリプトと共に生成されるテストコードを使って、通信テストにかかる時間を大幅に短縮できます。
f:id:inosyan:20180202233624p:plain

自動生成されるスクリプトはサーバーAPIに特化しているので、他のロジックと明確に分離することができ、可読性が向上します。
通信につかうクラスはUnity標準の“WWW”で、このパッケージ特有の機能は使っていません。生成したスクリプトはパッケージのdllには依存しておらず、書き出されたクラスをカスタマイズすることで、ヘッダー情報や送信データのフォーマットなどを調整できるので、あらゆる通信に対応できます。
JSONをパースするためのモデルクラスも自動生成されるので、JSONをそのまま使うより安全に扱うことができます。
f:id:inosyan:20180202233658p:plain

さらに、Pro版ではTSVファイルの読み書きができるので、エクセルなどの外部ツールと連携でき、Unityをもっていないサーバー開発者との連携が可能になります。
f:id:inosyan:20180202233716p:plain

このパッケージは、私自身が開発時に経験した問題点を解決したいと思って作りました。みなさまの開発が少しでも楽になれば幸いです。

全ての機能が利用可能なProバージョンはこちら
機能を限定したFreeバージョンはこちら

チュートリアル(英語)
チュートリアル(日本語)

UnityのTimelineを使ったムービー制作 #2

f:id:inosyan:20180126081931j:plain

先日の記事で、アセットストア向けにアセットパッケージの制作をした際に、紹介ムービーをUnityで作成した際のことを書きましたが、今回はその続きです。

このムービーの冒頭の10秒間の部分がUnityで制作した部分です。


WWW Script Generator

ムービー開始後3~4秒付近の光る部分は点光源の光量をスクリプトで制御しています。 f:id:inosyan:20180126082039g:plain

シーンには点光源が置いてありますが、はじめは非アクティブで、タイムライン上で時間が来たらアクティブにしています。それと同時に点光源の光量を制御するスクリプトを開始しています。 はじめは光量0でだんだん明るくなり、一番明るくなったところからすこし暗くなって終わります。 f:id:inosyan:20180126082104p:plain

PointLightOnAsset.cs

[System.Serializable]
public class PointLightOnAsset : PlayableAsset
{
    public ExposedReference<Light> PointLight;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        var handle = ScriptPlayable<PointLightOn>.Create(graph);
        var beh = handle.GetBehaviour();
        beh.PointLight = PointLight.Resolve(graph.GetResolver());
        return handle;
    }
}

PointLightOn.cs

using UnityEngine;
using UnityEngine.Playables;

public class PointLightOn : PlayableBehaviour
{
    public Light PointLight;

    public override void OnGraphStart(Playable playable) {
    }

    public override void OnGraphStop(Playable playable) {
    }

    public override void OnBehaviourPlay(Playable playable, FrameData info) {
    }

    public override void OnBehaviourPause(Playable playable, FrameData info) {
        AdjustIntensity(1);
    }

    public override void PrepareFrame(Playable playable, FrameData info) {
        float rate = (float)(playable.GetTime() / playable.GetDuration());
        float d = 120 * rate / 180 * Mathf.PI;
        d = Mathf.Sin(d) / Mathf.Sin(120.0f / 180.0f * Mathf.PI);
        AdjustIntensity(d);
    }

    void AdjustIntensity(float intensity)
    {
        if (!PointLight) return;
        PointLight.intensity = intensity;
    }
}

PlayableAssetを継承したスクリプトをタイムラインに配置し、点光源のインスタンスを紐付けます。具体的な挙動はPlayableBehaviourを継承したスクリプトに記述します。

タイムラインのポジションが動くときに呼ばれるPrepareFrameの中で、

float rate = (float)(playable.GetTime() / playable.GetDuration());

タイムライン上のこのスクリプトの範囲を0~1で表したときのどの位置にいるのかを調べ、 それを0~120°の角度に換算し、

float d = 120 * rate / 180 * Mathf.PI;

後半明るくなってから少し暗くなるように、sin(120°)が最終的な光量になるように調整しています。

d = Mathf.Sin(d) / Mathf.Sin(120.0f / 180.0f * Mathf.PI);

sinの値は

sin(0°) = 0
sin(90°) = 1
sin(120°) = 0.87

なので、それぞれsin(120°)で割ると

sin(0°) / sin(120°) = 0
sin(90°) / sin(120°) = 1.15
sin(120°) / sin(120°) = 1

となり、一番明るいときの光量は1.15なので最後の1と較べてすこし明るいため、一瞬スパークしたように見えるのです。

ところで、このムービーで紹介しているアセットパッケージは申請してから2週間経とうとしますが、まだ審査中です。以前別のアセットパッケージを公開した際も2週間ほどかかったのでそろそろではないかとは思いますが、公開されたらあらためてご紹介したいと思います。

「HoloLens 日本上陸1周年記念!コミュニティのみなさまへの大感謝祭!」に行ってきました

昨日、HoloLensのイベント「HoloLens 日本上陸1周年記念!コミュニティのみなさまへの大感謝祭!」に行ってきました。一ヶ月前に申込んだときは応募開始後すぐに定員に達してしまいキャンセル待ちだったのですが、当日になって運良く行けることになりました。

HoloLensが日本に上陸して1年、早いものです。事例も増えてきているようです。 f:id:inosyan:20180119082447j:plain

パネルディスカッションで話題にのぼってましたが、認定パートナーになるのは大変そうです。まず日本で面接があり、それからシアトルに行って面接を受け、そこにはHoloLensアプリ開発用のちゃぶ台の置いてあるスペースがあり、そこで数日研修をうけるそうです。 f:id:inosyan:20180119082452j:plain

日本国内で発売されている、Windows Mixed Realityヘッドセットが展示されていました。HoloLens以外のデバイスはイマーシブデバイス(現実の視界が透けて見えない没入型のデバイス)ですが、HoloLensと比べると値段は安いです。 シェアリング機能(複数デバイスで空間を共有)はイマーシブデバイスでも可能らしいです。検証機としてあったほうがいいかもしれません。 f:id:inosyan:20180119082503j:plain

HoloLens アプリ開発コンテストが開かれるそうです。応募期間は4/1~4月末。これはぜひ参加しようと思います。 f:id:inosyan:20180119082458j:plain

情報を体験として共有することがあたりまえになる未来、それに備えて今できることをやっていきたいです。 f:id:inosyan:20180119082500j:plain

HoloLensは会社に買ってもらい、去年HoloLens向けのアプリWindow Breakerを作りました。そのアプリの紹介はまたこんど書きますが、今回のイベントに参加し、これからもっとたくさんアプリを作っていきたいと思いました。

UnityのTimelineを使ったムービー制作

f:id:inosyan:20180115081455j:plain 先日仕事で、Unityアセットストア向けにアセットパッケージの制作をしました。そちらはいま申請中ですので審査が通ったらまたお知らせしますが、申請に必要だった紹介ムービーをUnityを使って作成したので、今回は会社に許可をもらい、そのムービーをご紹介します。

このムービーの冒頭の10秒間の部分がUnityで制作した部分です。


WWW Script Generator

モデリングはmayaで行いました。冒頭で一部だけを光らせたいので、マテリアルを分けてあります。
f:id:inosyan:20180115081506j:plain

あとはUnityの作業です。 はじめに周りの3つの三角形が点滅し、次に中央が点滅しますが、この部分のマテリアルには、ビルドインシェーダーの Unity スタンダードシェーダー を使用しており、Emission の値を有効にしています。 f:id:inosyan:20180115081703p:plain

そして、点滅させるためにシェーダーのプロパティをスクリプトで制御しています。 そのためには PlayableAsset, PlayableBehaviour をそれぞれ継承した2つのクラスを用意し、PlayableAssetを継承したほうをTimelineに配置します。今回は点滅する箇所は、周りの三角形と中央の2箇所ありますが、共通のスクリプトを使ってenumでマテリアルを切り替えています。
f:id:inosyan:20180115081725p:plain

StartEffectAsset.cs

using UnityEngine;
using UnityEngine.Playables;

[System.Serializable]
public class StartEffectAsset : PlayableAsset
{
    public StartEffect.TargetEnum Target;
    public Material TopMaterial;
    public Material CenterMaterial;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go) {
        var handle = ScriptPlayable<StartEffect>.Create(graph);
        var beh = handle.GetBehaviour();
        beh.Target = Target;
        beh.TopMaterial = TopMaterial;
        beh.CenterMaterial = CenterMaterial;
        return handle;
    }
}

StartEffect.cs

using UnityEngine;
using UnityEngine.Playables;

public class StartEffect : PlayableBehaviour
{
    public enum TargetEnum {
        Top,
        Center,
    }

    public TargetEnum Target;
    public Material TopMaterial;
    public Material CenterMaterial;

    Material TargetMat {
        get {
            switch(Target){
                case TargetEnum.Top: return TopMaterial;
                case TargetEnum.Center: return CenterMaterial;
                default: return TopMaterial;
            }
        }
    }

    void SetEmission(Color col)
    {
        if (TargetMat && TargetMat.HasProperty("_EmissionColor"))
            TargetMat.SetColor("_EmissionColor", col);
    }

    // Called when the owning graph starts playing
    public override void OnGraphStart(Playable playable) {
    }

    // Called when the owning graph stops playing
    public override void OnGraphStop(Playable playable) {
    }

    // Called when the state of the playable is set to Play
    public override void OnBehaviourPlay(Playable playable, FrameData info) {
        AdjustEmission(0);
    }

    // Called when the state of the playable is set to Paused
    public override void OnBehaviourPause(Playable playable, FrameData info) {
        AdjustEmission(1);
    }

    // Called each frame while the state is set to Play
    public override void PrepareFrame(Playable playable, FrameData info) {
        float rate = Mathf.Clamp01((float)(playable.GetTime() / playable.GetDuration()));
        AdjustEmission(rate);
    }

    void AdjustEmission(float rate)
    {
        var dark = new Color(0, 0, 0, 1);
        var light = new Color(0.362353f, 0.7f, 0.7f, 1);
        if (rate < 0.5) 
            SetEmission(dark);
        else
        {
            var d = rate * 270 / 180 * Mathf.PI;
            rate = Mathf.Abs(Mathf.Sin(d));
            SetEmission(rate < 0.5 ? dark : light);
        }
    }
}
}

PlayableBehaviourを継承したほうのスクリプトに、具体的な動きを記述します。
public override void PrepareFrame(Playable playable, FrameData info)
の中が、Timelineのスライダーが動くたびに呼ばれるところで、このスクリプトがアタッチされている箇所にスライダーが差しかかったときに、点滅の処理をします。

タイムライン上の範囲をrateという変数で表現します。rateは時間経過とともに0から1に変化します。このように、前半は光が消えている状態です。

if (rate < 0.5) 
    SetEmission(dark);

後半はrateを0~270°の角度に置き換え、sin式を使っています。

else
{
    var d = rate * 270 / 180 * Mathf.PI;
    rate = Mathf.Abs(Mathf.Sin(d));
    SetEmission(rate < 0.5 ? dark : light);
}

図にするとこんな感じです。
f:id:inosyan:20180115081802p:plain

再生するとこのようになります。
f:id:inosyan:20180115081823g:plain

プログラムでアニメーションを表現する際には、このようにsinをよくつかいます。
続きはまた後日書きたいと思います。

UnityのShaderによる映像制作の可能性

f:id:inosyan:20180107231719p:plain UnityShaderをさわっていたら、このような映像ができました。

f:id:inosyan:20180107232409p:plain f:id:inosyan:20180107233033p:plain


Light sphere

テクスチャを貼らないでShaderだけでどんな表現ができるのだろうと試していたら、このような映像ができました。
Unityのシーンは、球体にシェーダーを適用したマテリアルを貼り、その中をカメラが動いているシンプルなものです。
カメラのアニメーションにはTimelineを、ムービー用の連番画像の書き出しには Unityの Recorder を使いました。

f:id:inosyan:20180107234906p:plain

このシェーダーは、カメラからの距離によって模様や色が変わるようにしてあるので、カメラが動くことで万華鏡のような模様が現れます。

Shader "LightSphere" for Unity

自分で書いたShaderなら、テクスチャと標準のシェーダーとの組み合わせではできない面白いことができそうです。 しばらくShaderを色々試してみようと思います。