Nobollel開発者ブログ

Nobollelのエンジニアが、UnityやCocos2d-xの旬な情報・技術を紹介します。

【Reactive Extensions】 IObservableの合成と分岐入門その2

みなさん、おはこんばんにちは。tsuchimotoです。

今回はUniRx(UnityのRxライブラリ)を使ったIObservableの合成と分岐入門の2回目です。

Mergeメソッド

Mergeメソッドについて説明します。Mergeメソッドは複数のIObservableシーケンスを1つにまとめます。

このメソッドで2つのシーケンスを1つにした場合、そのシーケンスの発行タイミングは合成前と変わりません。

つまり、Mergeメソッドは複数のシーケンスを合成後もシーケンスの発行タイミングを変えずに、そのまま1つのシーケンスにできます。

Mergeメソッドのシグネチャは以下になります。

// firstとsecondを合成する
public static IObservable<T> Merge<T>(this IObservable<T> first, IObservable<T> second);

Mergeメソッドの使用例を以下に示します。

// a0,a1,a2と値を発行する
var s1 = Observable.Interval(TimeSpan.FromMilliseconds(250))
    .Take(3)
    .Select(i => "a" + i);
// b0,b1,b2,b3,b4と値を発行する
var s2 = Observable.Interval(TimeSpan.FromMilliseconds(150))
    .Take(5)
    .Select(i => "b" + i);

s1.Merge(s2).Subscribe(
    s => Debug.Log("OnNext: " + s),
    s => Debug.Log("Completed"));

実行結果を以下に示します。
合成前の2つのシーケンスの発行タイミングを保ったまま、1つのシーケンスにまとめられたことを示しています。

OnNext: b0
OnNext: a0
OnNext: b1
OnNext: a1
OnNext: b2
OnNext: b3
OnNext: a2
OnNext: b4

Mergeメソッドには以下のようなオーバーロードがあります。

// ①配列の全てをマージ
public static IObservable<T> Merge<T>(params IObservable<T>[] sources);
// ②返されたIObservable<T>をすべてマージ
public static IObservable<T> Merge<T>(this IObservable<IObservable<T>> sources);

①は引数で渡したIObservableの配列を全てマージします。コード例を下記に示します。

// a0,a1,a2と値を発行する
var s1 = Observable.Interval(TimeSpan.FromMilliseconds(250))
    .Take(3)
    .Select(i => "a" + i);
// b0,b1,b2,b3,b4と値を発行する
var s2 = Observable.Interval(TimeSpan.FromMilliseconds(150))
    .Take(5)
    .Select(i => "b" + i);

Observable.Merge(s1, s2).Subscribe(
    s => Debug.Log("OnNext: " + s),
    s => Debug.Log("Completed"));

実行結果は先ほどと同じになります。

②は少々複雑になります。 IObservable<T>を返すようなIObservableに対して使用でき、返されたIObservable<T>をすべてマージします。

コード例を下記に示します。

var source = new Subject<string>();
source
    // IObservable<string>からIObservable<IObservable<string>>に変換
    .Select(i => Observable
        // 0,1,2と値を3つ発行
        .Interval(TimeSpan.FromMilliseconds(250)).Take(3)
        // 値を変換
        .Select(j => i + j))
    // IObservable<IObservable<T>>からIObservable<T>へマージ
    .Merge()
    // 購読
    .Subscribe(
        i => Debug.Log("OnNext: " +  i),
        i => Debug.Log("OnCompleted"));

// 値の発行
source.OnNext("a");
source.OnNext("b");
source.OnNext("c");
source.OnNext("d");

実行結果を以下に示します。

値を3回発行するIObservable<string>を4つマージしています。

よって、値は12回発行されます。

OnNext: a0
OnNext: b0
OnNext: c0
OnNext: d0
OnNext: a1
OnNext: b1
OnNext: c1
OnNext: d1
OnNext: a2
OnNext: b2
OnNext: c2
OnNext: d2

②の形が理解できれば、次回に説明するSelectManyも理解できると思います。

Cocos2d-xをライブラリ化する手順

こんにちは。清水です。

従来利用されていたCocos2d-xプロジェクト管理ツールであった「Cocos」には、GUIでCocos2d-x用プロジェクトを簡単に作成することができました。しかし「Cocos」の開発が終わってしまったため、プロジェクト作成にはcocosコマンドから生成する必要があります。 「Cocos」にはライブラリ化されたCocos2d-xを利用できる機能があり、ゲーム開発を容易にしてくれていました。cocosコマンドにもその機能はありますが、少し導入手順が複雑ですのでここで紹介します。

1. Cocos2d-xのライブラリ化

cocos gen-libsコマンドにより、Cocos2d-xをライブラリ化します。弊社では開発時はMacアプリを作成しているため、iOSの他にMac向けのライブラリも作成します。ライブラリ化には時間がかかるため、休憩前など時間に余裕があるときに実行しましょう。開発中にCocos2d-xのログが必要な場合は、「-m debug」オプションを付加すればいいでしょう。また開発中にCocos2d-xのソースコードを修正する必要がある場合は、再ビルドする必要があります。

cocos gen-libs -p ios
cocos gen-libs -p mac

Androidに関しては現在バグがあるため、本ブログではスキップします。すでにGitHub上の開発版では修正済みでありv3.13では解消される予定です。(どうしても対応が必要な方は、GitHubからソースコードを取得し適用してください。)
https://github.com/cocos2d/cocos2d-x/issues/16068

ライブラリ化が終わると、Cocos2d-x直下のprebuildフォルダに、それぞれiOSMacのフォルダが作成され、その中にライブラリが格納されています。

f:id:nobollel:20160815153718p:plain

2. プロジェクト作成

いつものようにプロジェクトを作成します。ここではプロジェクト名「PrebuildTest」とし、開発言語にC++を選択しました。

cocos new PrebuildTest -l cpp

3. Cocos2d-xの参照先を変更

iOSMac用アプリのため、XcodeよりCocos2d-xの参照先を先ほど作成したライブラリに変更します。その手順を詳細に紹介します。

3-1. cocos2d_libsのTARGETSを削除

プロジェクトが参照しているcocos2d_libs.xcodeprojのTARGETSから下記を削除します。

  • libcocos2d Mac
  • libcocos2d iOS

f:id:nobollel:20160815153724p:plain

3-2. ライブラリのコピー

1.で作成したライブラリを直接プロジェクトにドラッグアンドドロップしてもいいですが、弊社ではチーム開発を行っているためパスを共通な位置としています。ここでは明示的にproj.ios_mac配下にlibsフォルダを作成し、その中にiOSMac用のライブラリを配置しました。

f:id:nobollel:20160815153729p:plain

3-3. ライブラリのリンク

プロジェクトのTARGETSから、それぞれiOSMac用のライブラリを追加します。

f:id:nobollel:20160815153735p:plain f:id:nobollel:20160815153743p:plain

3-4. ライブラリのパス設定

ライブラリがあるパスをプロジェクトに設定します。ここではproj.ios_mac配下にlibsフォルダを作成したので、パスは次のように設定しました。

$(SRCROOT)/libs

f:id:nobollel:20160815153751p:plain

4. 実行確認

iOSMacでアプリが正常に実行されることを確認します。もちろんCocos2d-xがライブラリ化されているので、すぐにアプリが実行されます。もし正常に動作しない場合は、再度手順を見直してください。

アプリはインストールされません

こんにちは、エンジニアの石橋です。 今回はアセットストアの話題は休憩して、最近テスト中に問題になった小ネタを紹介します。

Apkをインストールしようとしたら「アプリはインストールされません」というメッセージが出て困ったことありませんか?

どういう時に発生したか?

  • 開発中のアプリに問題があり修正をしていたところ、ビルドに時間がかかるので特定の部分を抜き出したプロジェクトを別に作り、インストールしようとした場合に発生
  • 以前インストールしたアプリはアンインストール済み
  • Android 5.0以降

原因は?

最初にインストールしたAPK(A)と後からインストールしようとしたAPK(B)が別々の署名で同じパーミッションの場合に発生します。

どう解決するか?

  1. Aを再度インストール
  2. Aをアンインストール(全てのユーザーから)
  3. Bをインストール

2が注意するところで、全てのユーザーからアンインストールしないと見た目には消えていてもデータが残っています。

終わりに

実はこの現象はターミナルからadbを使ってインストールしようとするとINSTALL_FAILED_DUPLICATE_PERMISSIONというエラーメッセージが出るので探せば直ぐに原因が判ります。 逆にAndroid File Transferやエクスプローラからファイルコピーをしていると、「アプリはインストールされません」メッセージのみでさっぱり原因が判らなくて困ります。

どなたかの参考になれば幸いです。

【Reactive Extensions】 IObservableの合成と分岐入門その1

みなさん、おはこんばんにちは。tsuchimotoです。
今回からReactive Extension(Rx)でも特に私が分かりづらいと思っているIObservableクラスの合成と分岐について説明したいと思います。

Reactive Extensionsでは、時間、イベント、非同期処理などをIObservableのシーケンス(イベントの流れ)として扱うことができます。
合成とは2本以上のイベントの流れを1本にまとめることです。
まずは分かりやすいメソッドから説明していきます。

Concatメソッド

Concatメソッドについて説明します。Concatメソッドは複数のIObservableのシーケンスを直列で繋いで1本の流れにします。

このメソッドで2本のシーケンスを1本にした場合、1本目のシーケンスがすべて完了してから2本目のシーケンスが開始されます。

3本のシーケンスを1本にした場合、1本目のシーケンスがすべて完了してから2本目のシーケンスが開始され、2本目のシーケンスがすべて完了してから3本目のシーケンスが開始されます。

Concatメソッドのシグネチャを以下になります。

// firstとsecondをつなぐ
public static IObservable<T> Concat<TSource>(this IObservable<TSource> first, IObservable<T> second
)

Concatメソッドの使用例を以下に示します。

Observable
    // 0,1,2と値を発行する
    .Range(0, 3).Select(i => "1st: " + i)
    .Concat(
        // 5,6,7,8,9と値を発行する
        Observable.Range(5, 5).Select(i => "2nd: " + i)
    )
    .Subscribe(s => Debug.Log("OnNext: " + s));

実行結果を以下に示します。
最初のシーケンス(1st)がすべて完了後に次のシーケンス(2nd)が開始されているのが分かると思います。

OnNext: 1st: 0
OnNext: 1st: 1
OnNext: 1st: 2
OnNext: 2nd: 5
OnNext: 2nd: 6
OnNext: 2nd: 7
OnNext: 2nd: 8
OnNext: 2nd: 9

実行結果を図にすると以下のようになります。

1st --0--1--2-|
2nd           -5--6--7--8--9|
r   --0--1--2--5--6--7--8--9|

Concatメソッドには以下のようなオーバーロードがあります。

// ①配列のすべての要素をつなぐ
public static IObservable<T> Concat<T>(IObservable<T>[] sources)
// ②IEnumerable<IObservable<T>>から取得できるすべての要素をつなぐ
public static IObservable<T> Concat<T>(this IEnumerable<IObservable<T>> sources)
// ③IObservable<IObservable<T>>から発行されるすべての要素をつなぐ
public static IObservable<T> Concat<TSource>(this IObservable<IObservable<T>> sources)
)

①のConcatメソッドの使用例を以下に示します。

Observable
    .Concat(
        // 0,1,2と値を発行する
        Observable.Range(0, 3).Select(i => "1st: " + i),
        // 5,6,7,8,9と値を発行する
        Observable.Range(5, 5).Select(i => "2nd: " + i)
    )
    // 講読
    .Subscribe(s => Debug.Log("OnNext: " + s));

実行結果は最初のコード例とまったく同じになります。

StartWithメソッド

StartWithメソッドについて説明します。
StartWithメソッドのシグネチャを以下に示します。

public static IObservable<T> StartWith<T>(this IObservable<T> source, params T[] values);

このメソッドはIObservableのシーケンスの先頭にvaluesで指定した要素を合成して1つのシーケンスにします。
このメソッドの使用例を以下に示します。

Observable
    // 1,2,3,4と値を発行するシーケンス
    .Range(1, 4)
    // 頭に10,20,30をつける
    .StartWith(10, 20, 30)
    // 購読
    .Subscribe(s => Debug.Log("OnNext: " + s));

実行結果を以下に示します。
StartWithで付けた値が初めに発行されているのが分かると思います。

OnNext: 10
OnNext: 20
OnNext: 30
OnNext: 1
OnNext: 2
OnNext: 3
OnNext: 4

Cocos2d-x v3.11においてCocosStudioを利用する手順

こんにちは。清水です。前回、新しいCocos2d-x用のツールとしてCocos Creatorを紹介しました。しかし、この登場によりこれまで利用されていたCocosStudioの開発が停止してしまい、CocosStudio上でCocos2d-x v3.11のプロジェクトの作成を行うことができなくなってしまいました。試しにCocos.appが利用するCocos2d-xのフォルダにCocos2d-x v3.11を配置し、プロジェクト作成時にCocos2d-x v3.11を選択しても「Operation is in progress, please wait...」から先に進みません。

f:id:nobollel:20160622150557p:plain

これはCocos2d-xがCocosStudioのサポートを止めたわけではなく、今後もこのCocosStudioが利用できます。実際、2年前まで多くの開発者に利用されていたCocosBuilderでさえ、今でも利用することが可能です。 今回は、Cocos2d-x v3.11でCocosStudioを利用する方法をご紹介します。

(1) CocosStudioでプロジェクトを作成

従来の通りCocos上でプロジェクトを作成します。このとき、Engine Versionは何を選択しても問題ありません。またEditorとして「Cocos Studio」にチェックを入れておきましょう。

f:id:nobollel:20160622151030p:plain

(2) Cocos2d-x v3.11でプロジェクトを作成

CocosStudioとは別にCocos2d-xに備わっているCocosコマンドより、新しくプロジェクトを作成します。

f:id:nobollel:20160622150540p:plain

(3) プロジェクトを置き換える

(1)で作成したプロジェクトからCocosStudioに必要な以下のファイルを、2.で作成したプロジェクトに移動します。

  • [Project Name].ccs
  • [Project Name].cfg
  • [Project Name].udf
  • cocosstudioフォルダ

f:id:nobollel:20160622150542p:plain

(4) iOS用にCocosStudioを利用するための設定を行う

(2)で作成したプロジェクトはAndroidではすぐにCocosStudioを利用することができますが、iOSにてCocosStudioを利用するための設定が行われていません。そのため、次の設定を行いましょう。  プロジェクトの「Build Settings > Search Paths > Header Search Paths」に「$(SRCROOT)/../cocos2d/cocos/editor-support」を追加します。

f:id:nobollel:20160622151015p:plain

(5) CocosStudio上でリソースを書き出す

(3)にてCocosStudioを移動したばかりですので、CocosStudioのファイルが出力されていません。CocosStudio上にてリソースを書き出しましょう。

f:id:nobollel:20160622151005p:plain

(6) Cocos2d-xのソースでCocosStudioのファイルを読み込む

HelloWorldScene.cppなどにCocosStudioのファイルを読み込む処理を追記します。

bool HelloWorld::init() {
    if (!Layer::init()) {
        return false;
    }
    
    //CocosStudioファイルの読み込み
    auto rootNode = CSLoader::createNode("res/MainScene.csb");
    rootNode->setPosition((Director::getInstance()->getWinSize() - rootNode->getContentSize()) / 2);
    addChild(rootNode);

    return true;
}

またCocosStudioの標準では、リソースファイルをResources > resに配置するように設定されているため、AppDelegate.cppのapplicationDidFinishLaunching関数に次の1行を加えておくといいでしょう。

//参照パスとしてresフォルダを追加する
FileUtils::getInstance()->addSearchPath("res");

(7) アプリ実行

上記が全て終わったらアプリを実行してみましょう。無事起動できると次のように表示されます。

f:id:nobollel:20160622151731p:plain

Cocos2d-xのバージョンが上がっても、CocosBuilderと同様にCocosStudioもサポートされ続けるでしょう。CocosCreatorまたは適切なエディタが登場するまでは、しばらくCocos Studioを利用するといいでしょう。

実機(iPhone, Android)でのデバッグ

こんにちは、石橋です。

みなさんは実機(iPhone, Android)でのデバッグはどのようにされていますか?
エディタ上では停止ボタンでインスペクタからパラメータを弄ることも簡単にできますが、実機ではそうもいきませんよね?
Unityには予めGUIが備わっていてボタンやテキストフィールドなどを表示しすることも出来ますが、作るのは結構大変です。
そこで、今回はサクッとデバッグメニューが作れるおすすめのアセットを紹介します。

SRDebugger

お値段30$(記事記載時)

ダウンロード&インストールすると直ぐに使用できます。
実行して画面左上を3回タップするとデバッグメニューが開きます。(オプションで変更できます)

f:id:nobollel:20160708140301p:plain

画面左にSystem, Console, Options, Profilerというボタンがあります。
Systemは端末の情報を、ProfilerはFPSやメモリ使用量を見ることが出来ます。
ConsoleはEditorのConsoleと同じものを出してくれるのでログを仕込んでおき、テストプレイヤーからの報告にも使えます。

さて、本題のOptionsに移りましょう。
これを利用するにはスクリプトを書く必要があります。

using UnityEngine;
using System.Collections;
using System.ComponentModel;
    
public partial class SROptions {
  [Category( "Hoge" )]
  [DisplayName( "Fuga" )]
  public void HogeHoge() {
    Debug.Log( "PiyoPiyo" );
  }
}   

このように記載すると以下のような表示になります。

f:id:nobollel:20160708140316p:plain

HogeというカテゴリでFugaというメソッドが作成できました。
画面上のFugaの部分はボタンになり、押すとPiyoPiyoというログが表示されます。
Categoryを使用するにはSystem.CombonentModelのusingが必要になります。
DisplayNameはメソッドと違う表示にしたい場合に使います、この部分は日本語も可能です。

今度はプロパティを追加してみましょう。

    [Category( "Hoge" )]
    [DisplayName( "HP" )]
    public int HP {
        get;
        set;
    }

f:id:nobollel:20160708140351p:plain

<> で数字の加減算が出来る他、数値をクリックするとキーボードで直接設定も出来ます。
Propertyの場合、setをprivate setとすることにより読み取り専用にも出来ます。
bool値の場合はチェックボックスで表示されます。

メソッドやプロパティを増やしていくと表示がごちゃごちゃになりますのでSort属性を使って順番を設定することもできます。

using UnityEngine;
using System.Collections;
using System.ComponentModel;

public partial class SROptions {
    [Category( "Hoge" )]
    [DisplayName( "Fuga" )]
    [Sort( 0 )]
    public void HogeHoge() {
        Debug.Log( "HogeHoge" );
    }

    [Category( "Hoge" )]
    [DisplayName( "HP" )]
    [Sort( 1 )]
    public int HP {
        get;
        set;
    }

    [Category( "Hoge" )]
    [DisplayName( "Readonly" )]
    [Sort( 2 )]
    public int ReadonlyProperty {
        get;
        private set;
    }
}

Sortを設定すると以下のようになります。

f:id:nobollel:20160708140416p:plain

如何でしたか?
基本的な部分のみですが、これだけでも簡単にデバッグ環境を構築できることが判っていただけるかと思います。
他にも属性やAPIが多数あるので公式ドキュメントを参照して下さい。
この記事が皆さんのお役に立てたら幸いです。

UniRxを使ったダウンロード時のプログレスバーの実装

こんにちは、Nobollel株式会社でエンジニアをしている土本です。

Nobollelではモバイルゲーム開発にUnityを使っていて、RxのライブラリとしてUniRxを積極的に使用しています。

みなさんは複数URLのコンテンツダウンロードのプログレスバーやダウンロードが完了した個数の表示はどのように実装していますでしょうか?

今回はゲーム起動時にアセットバンドルを連続してダウンロードし、プログレスバーに進捗を反映させるときに役に立つtipsを紹介したいと思います。

UniRxでいくつものURLに連続してHTTPリクエストをする場合、WWWObservableとクエリ構文を使って簡単に実装できます。

IObservable<byte[]> Request(string url) {
    return ObservableWWW.GetAndGetBytes(url);
}

var query = from first in ("assetbundle_url1")
    from second in Request("assetbundle_url2")
    from third in Request("assetbundle_url3")
    select new { first, second, third };
    
var cancel = query.Subscribe(result => OnCompleted(result));

ただしこの方法だと、1番目と2番目のリクエストの完了を検知できません。 そこで、クエリ構文を使わずに書くとこうなります。

void SaveBytes(byte[]) {
    // アセットバンドルのデータを保存
}

void UpdateProgressBar() {
    // プログレスバーの更新
}

Request("assetbundle_url1")
    .SelectMany(bytes => {
        SaveBytes(bytes);
        UpdateProgressBar();
        return Request("assetbundle_url2");
    })
    .SelectMany(bytes => {
        SaveBytes(bytes);
        UpdateProgressBar();
        return Request("assetbundle_url3");
    })
    .Subscribe(bytes => OnCompleted(bytes));

これで、リクエストの完了ごとにプログレスバーの更新ができるようになりました。

予めリクエストする回数が分かっているときはこれで良いのですが、ダウンロードするアセットバンドルの数(リクエスト数)が可変のときは、この方法は使えません。

そこで、次のような方法で可変個数に対応します。

List<string> urls = new List<string>{"url1", "url2", "url3"};
urls.Select(url => {
    return Observable.Defer(() => {
        return Request(url);
    });
}).Aggregate((x, y) => x.SelectMany(bytes => {
    SaveBytes(bytes);
    UpdateProgressBar();
    return y;
})).Subscribe(bytes => {
    SaveBytes(bytes);
    UpdateProgressBar();
});

まず最初のSelectでstringをIObservable<byte[]>に変換します。 そのあとAggregateとSelectManyを使って、2つ目のリクエスト以降を前のリクエストの通知が来たら次のリクエストを開始するように変換します。 このようにすることによって、数珠つなぎのように連続してリクエストを送ることができます。

展開すると以下のような形になります。 (分かりやすくするためレスポンス取得後のSaveBytesとUpdateProgressBarの呼び出しを省略しています)

  • 1回目のxにrequest1が入る
var request1 = Observable.Defer(() => Request(url1));
  • 2回目のxにrequest2が入る
var request2 = request1.SelectMany(_ => Observable.Defer(() => Request(url2)));
  • 3回目のxにrequest3が入る
var request3 = request2.SelectMany(_ => Observable.Defer(() => Request(url3)));

最後にSubscribeします

request1.Subscribe();
request2.Subscribe();
request3.Subscribe();

いかがでしたでしょうか? ダウンロード処理を実装する際、このtipsが参考になれば嬉しいです。

ただいまNobollelではUnityエンジニア、プログラマーを絶賛募集中です。 興味のある方はご連絡ください! nobollel.com