Nobollel開発者ブログ

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

PoolManager

みなさん、おはこんばんにちは。
エンジニアの石橋です

今回はPoolManagerの紹介です。

Unityでゲームを作る場合はPrefabにしたオブジェクトを生成、破棄を繰り返す場面が多いと思います。 Prefabの生成と破棄はUnityでは割りと重い処理なので、生成を一度にしたり、一度作ったオブジェクトは破棄せず保持するようなプールシステムを良く作りますがPoolManagerでは基本機能プラスもう少し便利にしたアセットです。

f:id:nobollel:20161220201113p:plain

  1. 適当にオブジェクトを作ります
  2. Pool Nameを設定します。
    これはstatic classのPoolManager.PoolsからSpawnPoolを検索する際のKeyになります。
  3. Pre-Prefab Pool Optionsの右側にある+ボタンを押して追加します
  4. 生成するオブジェクトのプレハブを設定します
  5. 起動後に自動的に生成するプレハブの個数を設定します。
    これはactiveSelfがfalseになっているオブジェクトで生成されます。

生成するにはSpawnを呼び出します。 生成したプレハブを破棄(deactiveに)するにはSpawnの戻り値で受け取ったTransformをDespawnに渡します。

public class Hoge : MonoBehaviour {
    [SerializeField] SpawnPool pool;

    public Transform Spawn() {
        return pool.Spawn( "Cube" );  
    }
    
    public void Despawn( Transform instance ) {
        pool.Despawn( instance );
    }
}

pool.Spawnの引数はプレハブ名を設定していますが、メソッドのオーバーロードでプレハブそのものを指定することも出来ます。
AudioSourceを指定すると再生が終わった場合に自動的にDespawnされます。


生成する個数に制限をかけることも出来ます。

f:id:nobollel:20161220201120p:plain

  1. limit Instancesにチェックをします。
  2. limit Amountに最大生成個数を設定します。
  3. limit FIFOはオプショですが、これにチェックを入れると最初に生成されたものが破棄され、新しく作られます。
    チェックが入っていない場合はpool.Spawnの戻り値がnullになります。

他にも一度に生成すると負荷がかかるので徐々に生成するオプションなどもあります。

Unityで実機上のアプリバージョンを取得する (iOS/Android)

Unityで実機上のアプリバージョンを取得する (iOS/Android)

こんにちは、Nobollelエンジニアの古屋です。今回は、Unity iOS/Android実機でバージョンを取得する方法を紹介します。


一応、アプリバージョンはApplication.versionから取得できます。

Debug.Log("version=" + Application.version);

しかし、これは表示用のバージョンで、厳密なバイナリバージョン (Androidならバージョンコード) が別にあります。ログにバージョンを乗せたいときなど厳密なバージョンの方を知りたいことも多いです。この記事ではこれらを実行時に取得する方法について書いていきます。

iOSのビルド番号を取得する

そもそも「ビルド番号」というのは、Unity上で設定するこれ↓

f:id:nobollel:20161209125706p:plain

もしくはXcode上のこれ↓のことです。

f:id:nobollel:20161209125911p:plain

(正式名称がよくわかりませんが、Unityのエディタスクリプトのプロパティ名がbuildNumberなので、便宜上ビルド番号と呼んでいます)

「Version」がApp Store上に表示されるバージョンなのに対して、「Build」はアップロードする各バイナリのユニークなバージョンです。たとえば、表示上のバージョンを変えずにバイナリをストアに再アップロードする場合は、Buildの数字を上げる必要があります。

ビルド番号の取得には、ネイティブコードの実装が必要です:

IOSVersion.mm (Plugins/iOS以下に配置)

extern "C" {
    char *GetBundleVersion() {
        NSString *bundleVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
        const char *s = [bundleVersion UTF8String];
        return strcpy((char *)malloc(strlen(s) + 1), s);
    }
}

[NSBundle mainBundle]infoDictionaryからバージョン情報を取り出して返すだけのネイティブコードです。CFBundleVersionというキーがビルド番号に該当します。

プラグインが返す文字列はヒープ上に確保されている必要がある (ここiOS Tipsを参照) ので、mallocしたポインタにstrcpyして返しています。


次はC#側のコードです:

IOSVersion.cs

#if UNITY_IOS
using System.Runtime.InteropServices;
#endif

#if UNITY_EDITOR
using UnityEditor;
#endif

public static class IOSVersion {
    public static string GetBuildNumber() {
#if UNITY_EDITOR
        return PlayerSettings.iOS.buildNumber;
#elif UNITY_IOS
        return GetBundleVersion();
#else
        return null;
#endif
    }

#if UNITY_IOS
    [DllImport("__Internal")]
    static extern string GetBundleVersion();
#endif
}

C#側では、さきほどのネイティブの関数2つをラップしているだけです。エディタ上ではプラグインが動作しないのでPlayerSettingsから値を取得して返し、iOS以外のプラットフォームではnullを返します。

Androidのバージョンコードを取得する

Androidでは、iOSのビルド番号に相当するものとしてバージョンコードがあります。こちらはネイティブコードを書かなくても、AndroidJavaObjectを使って実装できます。

AncroidVersion.cs

#if UNITY_ANDROID
using UnityEngine;
#endif

#if UNITY_EDITOR
using UnityEditor;
#endif

public static class AndroidVersion {
    public static int GetVersionCode() {
#if UNITY_EDITOR
        return PlayerSettings.Android.bundleVersionCode;
#elif UNITY_ANDROID
        using (var packageInfo = GetPackageInfo()) {
            return packageInfo.Get<int>("versionCode");
        }
#else
        return 0;
#endif
    }

#if UNITY_ANDROID
    static AndroidJavaObject GetPackageInfo() {
        using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
        using (var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
        using (var context = currentActivity.Call<AndroidJavaObject>("getApplicationContext"))
        using (var packageManager = context.Call<AndroidJavaObject>("getPackageManager"))
        using (var packageManagerClass = new AndroidJavaClass("android.content.pm.PackageManager")) {
            string packageName = context.Call<string>("getPackageName");
            int activities = packageManagerClass.GetStatic<int>("GET_ACTIVITIES");
            return packageManager.Call<AndroidJavaObject>("getPackageInfo", packageName, activities);
        }
    }
#endif
}

GetPackageInfoは見た目ごちゃごちゃしていますが、アクティビティからPackageManagerを取ってきてgetPackageInfoしているだけです。戻り値のPackageInfoクラスがバージョン情報を持っており、そこからversionCodeを取得します。

CocosCreatorやCocos2d-x(JS)で利用できるCoffeeScript

こんにちは。清水です。今回は、CocosCreatorやCocos2d-x(JS)で利用できるCoffeeScriptを紹介します。

CocosCreator

CocosCreatorは、一般的にJavaScript向けプロジェクトで利用するエディタですが、リリース当初よりデフォルトでCoffeeScriptもサポートされています。ここではCoffeeScriptの詳しい説明は割愛しますが、CocosCreatorでは、自動でCoffeeScriptJavaScriptコンパイルしてくれます。

試しにHelloWorldをCoffeeScriptで書いてみましょう。

CoffeeScript
cc.Class
    extends: cc.Component

    properties:
        label:
            default: null
            type: cc.Label
        text: 'Hello, World!'


    onLoad: () ->
        @label.string = @text

そしてブラウザで実行すると、いつものように実行できます。そのソースコードを確認すると、次のようにJavaScriptに変換されていることがわかります。

JavaScript
"use strict";
cc._RFpush(module, '04afax9CbNGhaoEgGXM/B//', 'HelloWorld');
// Script/HelloWorld.coffee

(function() {
  cc.Class({
    "extends": cc.Component,
    properties: {
      label: {
        "default": null,
        type: cc.Label
      },
      text: 'Hello, World!'
    },
    onLoad: function() {
      return this.label.string = this.text;
    }
  });

}).call(this);

cc._RFpop();

Cocos2d-x

ではCocos2d-x(JS)でもCoffeeScriptを利用してみましょう。しかしCocos2d-x(JS)はデフォルトでCoffeeScriptはサポートされていません。私が今まで利用した中では、Jitterを利用する方法が最も簡潔だと思います。()

コマンド
cocos new CoffeeTest -l js
cd CoffeeTest/src
jitter ./ ./ --bare

app.coffeeファイルを作成し、次のようにソースコードを入力すると、jitterが自動でapp.jsを更新してくれます。これを実行すると、いつものHello World画面が表示されます。

CoffeeScript
HelloWorldLayer = cc.Layer.extend
  sprite: null
  ctor: () ->
    @_super()

    size = cc.winSize
    helloLabel = new cc.LabelTTF("Hello World", "Arial", 38)
    helloLabel.x = size.width / 2
    helloLabel.y = size.height / 2 + 200
    @addChild(helloLabel, 5)

    @sprite = new cc.Sprite(res.HelloWorld_png)
    @sprite.attr
      x: size.width / 2
      y: size.height / 2
    @addChild(@sprite)

    true;

HelloWorldScene = cc.Scene.extend
  onEnter: () ->
    @_super()
    layer = new HelloWorldLayer()
    @addChild(layer)

CocosCreatorやCocos2d-xでCoffeeScriptを扱うことは簡単ですね。JavaScriptよりも記述するコード量が少なくなるので開発効率も上がることでしょう。

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

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

今回は前回の第3回に引き続き、UniRxを使ったIObservableの合成と分岐入門の4回目になります。

前回までのリンク

Zipメソッド

Zipメソッドについて説明します。

Zipメソッドは2つの合成元のシーケンスから流れてきた値が2つ揃った時点で初めて、合成後のIObservableのシーケンスに値が流れます。

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

public static IObservable<R> Zip<T, U, R>(
    // 1つ目のシーケンス
    this IObservable<T> first, 
    // 2つ目のシーケンス
    IObservable<U> second, 
    // 合成後のシーケンス
    Func<T, U, R> resultSelector);

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

1秒間隔で0, 1, 2の値を発行するIObservableのシーケンスと100, 101, 102の値を発行するIObservableのシーケンスをZipメソッドで合成しています。

// 1秒間隔で0,1,2と発行される
Observable.Interval(TimeSpan.FromSeconds(1)).Take(3)
    // Zipでまとめる
    .Zip(
        // 100, 101と発行される
        Observable.Range(100, 2), 
        // 流れてきた値を元に文字列化
        (l, r) => string.Format("{0}:{1}", l, r))
    // 購読
    .Subscribe(
        s => Debug.Log("OnNext: {0}", s),
        () => Debug.Log("OnCompleted"));

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

2つの発行された値が1つのシーケンスになって流れてくることが確認できると思います。

ただしここで注意したいのは、1番目のシーケンスから発行された「3」が2番目のシーケンスと揃わないので後続に流れてきません。

OnNext: 0:100
OnNext: 1:101
OnCompleted

CombineLatestメソッド

CombineLatestメソッドについて説明します。

Zipメソッドが両方のIObservableのシーケンスから値が発行されるのを待つのに対してCombineLatestメソッドは、 どちらか一方から値が発行されると、もう一方の最後に発行した値があるか確認し、あればそれを使って後続に値を流します。

このメソッドのシグネチャを以下に示します。

public static IObservable<R> CombineLatest<F, S, R>(
    this IObservable<F> first, 
    IObservable<S> second, 
    Func<F, S, R> resultSelector);

このメソッドのコード例を下記に示します。

先ほどのZipメソッドのコードとほぼ同じでZipメソッドがCombineLatestメソッドになっただけです。

// 1秒間隔で0,1,2と発行される
Observable.Interval(TimeSpan.FromSeconds(1)).Take(3)
    // CombineLatestでまとめる
    .CombineLatest(
        // 100, 101と発行される
        Observable.Range(100, 2), 
        // 流れてきた値を元に文字列化
        (l, r) => string.Format("{0}:{1}", l, r))
    // 購読
    .Subscribe(
        s => Debug.Log("OnNext: {0}", s),
        () => Debug.Log("OnCompleted"));

このコードの実行結果を以下に示します。 始めに「100」が発行された後に2はつ目のシーケンスが発行されないので、後続には流れてきません。 「101」発行後に「0」が発行されて初めて後続に流れてきます。

OnNext: 0:101
OnNext: 1:101
OnNext: 2:101
OnCompleted

Missing Push Notification Entitlement

みなさんおはこんばんにちは、エンジニアの石橋です。

今回はPush通知に付いて話そうかと思います。
Xcode8からはPush Notificationに関する署名をきちんと設定しなければいけなくなりました。
設定しないと申請時にMissing Push Notification Entitlementという内容のメールで却下されます。
これに対応するのは簡単でCapabilities -> Push NotificationsをONとすれば自動的にAppName.entitlementsファイルが作成されます。
ただ、これをビルド毎にやるのは少々面倒なので自動化します。

1) 以下の内容のファイルをAssets以下に配置します ファイル名はproduction.entitlementsとします。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>production</string>
</dict>
</plist>

2) Unityのビルドを自動化します。
以下の内容のファイルをAssets/Editor以下に配置します。 こちらはファイル名はなんでもよいです。

using UnityEditor;
using UnityEngine;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
using System.IO;

public class Hoge {
    static void OnPostProcessBuild(BuildTarget buildTarget, string buildPath ) {
        string projPath = buildPath + "/Unity-iPhone.xcodeproj/project.pbxproj";

        PBXProject proj = new PBXProject();
        proj.ReadFromString( File.ReadAllText( projPath ) );

        string target = proj.TargetGuidByName( "Unity-iPhone" );

        var debug = proj.BuildConfigByName( target, "Debug" );
        var release = proj.BuildConfigByName( target, "Release" );

        var fileName = "production.entitlements";
        var src = Path.Combine( Application.dataPath, fileName );
        var dest = Path.Combine( buildPath, fileName );
        File.Copy( src, dest );
        proj.AddFileToBuild( target, proj.AddFile( dest, fileName, PBXSourceTree.Source ) );
        proj.SetBuildPropertyForConfig( debug, "CODE_SIGN_ENTITLEMENTS", fileName );
        proj.SetBuildPropertyForConfig( release, "CODE_SIGN_ENTITLEMENTS", fileName );

        proj.WriteToFile( projPath );
    }
}

3) 最後にipa出力後にきちんと設定されているかを確認します。

  1. 出力したipaをzipにリネームして解凍
  2. Terminalで1で解凍したPayloadフォルダへ移動
  3. codesign -d --entitlements :- Payload/パッケージ.appとコマンドを打ちproductionで設定されていればOKです。

参考1 参考2

UnityでカスタムURLスキーム (iOS編)

こんにちは、Nobollelエンジニアの古屋です。今回は、UnityでカスタムURLスキームを実装する方法を紹介します。

カスタムURLスキームとは

カスタムURLスキームは、myapp://hogeのような独自のURLからアプリを起動するための仕組みです。これを使えば、Web上のリンクから自分のアプリを起動したり、起動中のあるアプリから別のアプリを立ち上げたりすることができます。

カスタムURLスキームはUnityに統合された機能ではないため、プラットフォームごとに実装する必要があり、この記事ではiOSの実装についてお話します。まずは、単純に自分のアプリをカスタムURLスキームから開けるようにし、次に、アプリ自身がURLスキームによって開かれたことを検知できるようにします。

1. アプリをカスタムURLから開けるようにする

アプリをカスタムURLスキームで開けるようにするのは簡単で、Info.plistに設定を行うだけです。

Info.plistの編集

UnityをiOS向けにビルドするとXcodeプロジェクトが書き出されますが、そのプロジェクト内にあるInfo.plistの中にCFBundleURLTypesとその値を追加します。

編集例:

<?xml version="1.0" encoding="utf-8"?>
<plist version="1.0">
  <dict>
    <!-- ここから -->
    <key>CFBundleURLTypes</key>
    <array>
      <dict>
        <key>CFBundleURLName</key>
        <string>test_name</string>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>test_scheme</string>
        </array>
      </dict>
    </array>
    <!-- ここまで -->
  </dict>
</plist>

CFBundleURLSchemesの値 (test_scheme) が実際のURLスキームになります。ようするに、test_scheme://のようなURLでアプリを起動することができます。

CFBundleURLNameの値 (test_name) が何に使われるのかはよくわからないのですが、Appleのガイドによれば、「URLスキームの抽象名」となっており、ユニークな値なら何でも良く、com.mycompany.myschemeのような形式が推奨ということです。

自動化

ビルドするたびにInfo.plistを編集するのは面倒だし忘れてしまうこともあるので、PostProcessBuildを使って、ビルドの度に設定が自動で行われるようにすると便利です。自動化を行うには、次のようなクラスをEditorフォルダの下に配置します。

using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;

public class MyPostprocessor {
    [PostProcessBuild]
    public static void OnPostprocessBuild(BuildTarget buildTarget, string path) {
        EditInfoPlist(path);
    }

    static void EditInfoPlist(string directory) {
        var path = Path.Combine(directory, "Info.plist");
        var document = new PlistDocument();
        document.ReadFromFile(path);

        // URLスキームを追加
        var urlTypes = document.root.CreateArray("CFBundleURLTypes");
        var dict = urlTypes.AddDict();
        dict.SetString("CFBundleURLName", "test_name");
        var urlSchemes = dict.CreateArray("CFBundleURLSchemes");
        urlSchemes.AddString("test_scheme");

        document.WriteToFile(path);
    }
}

Info.plistのXML構造を把握していれば、さきほどの設定をコード上で行っていることがわかると思います。

2. アプリ側でURLスキームから開かれたことを検知する

アプリがURLスキームを開かれたことを検知するには、iOSプラグインを実装する必要があります。

Objective-C側の実装

次のようなクラスを作り、Plugins/iOSフォルダの下に配置します。

MyAppController.mm

#import "UnityAppController.h"

void UnitySendMessage(const char* obj, const char* method, const char* msg);

@interface MyAppController : UnityAppController
@end

@implementation UrlHandlerAppController
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
    const char *url = [[url absoluteString] cStringUsingEncoding:NSUTF8StringEncoding];
    UnitySendMessage("MyUrlReceiver", "OnOpenUrl", url);
    return YES;
}
@end

IMPL_APP_CONTROLLER_SUBCLASS(UrlHandlerAppController)

やるべきことは、

  1. UnityAppControllerの子クラス (MyAppController) を作成
  2. application:openURL:options:を実装してURLを受け取り、UnitySendMessageでUnity側に返す
  3. IMPL_APP_CONTROLLER_SUBCLASSマクロに作成したクラス名を渡す

の3つです。

IMPL_APP_CONTROLLER_SUBCLASSはUnityが定義しているマクロで、指定したカスタムコントローラをデリゲートに設定しコールバックを受け取れるようにしてくれます。

Unity側の実装

さきほど呼び出したUnitySendMessageを受け取るだけです。

次のようなスクリプトを用意し、シーン上のゲームオブジェクトに追加します。ゲームオブジェクトの名前と、コールバックを受け取るメソッドの名前は、UnitySendMessageで指定した名前 (MyUrlReceiver, OnOpenUrl) と同じである必要があります。

MyUrlReceiver.cs

using UnityEngine;

public class MyUrlReceiver : MonoBehaviour {
    void OnOpenUrl(string url) {
        // TODO
    }
}

Cocos2d-x(JS)のためのatomプラグイン

Cocos2d-x(JS)で開発を進めるとき、私はIDEとしてWebStormの利用をオススメしていますが、無料のエディタであるatomを利用しているユーザも多いと思います。ただしatomを標準で利用していては開発効率が悪いので、プラグインを追加していると思います。今回はCocos2d-x(JS)を用いたアプリ開発に便利なプラグインを紹介します。

  1. autocomplete-cocos2d-js
  2. build-cocos
  3. jshint

atomプラグイン

  1. autocomplete-cocos2d-js
    atom.io このプラグインは、Cocos2d-x(JS)用の自動補完を行ってくれます。
    特別な設定は必要なく、プラグインをインストールするとすぐに利用可能となります。
    使い方も簡単で、エディタ上で3文字の入力で補完一覧が表示されます。データ元は公式のAPIリファレンスなので安心して利用できます。 f:id:nobollel:20161103100337p:plain
  2. build-cocos
    atom.io このプラグインは、atom上でプロジェクトのビルド・実行などを行うことができます。
    条件としてcocos-consoleが利用できる状態にあることですが、ほとんどの方はcocos2d-xをインストールしているので問題ないでしょう。
    Win: Ctrl + Alt + T, Mac: Command + Alt + T を押すと、実行可能な一覧が表示されるので、そこから適切なコマンドを選択するだけです。 f:id:nobollel:20161103100347p:plain f:id:nobollel:20161103100355p:plain
  3. jshint
    atom.io このプラグインは、JavaScriptの構文チェックツールです。
    JavaScriptは多様な書き方を許す言語であるため、同じ処理でもプログラマによってコードが異なります。個人で開発する分には問題ないのですが、チームで開発するとなると全体的な可読性が低下し開発効率が悪くなってしまいます。
    そのため構文チェックツールは必須となるでしょう。このプラグインはインストールすると、設定なしですぐに利用することができるので便利です。より本格的な構文チェックを行いたい場合は、linter-eslintをオススメします。 f:id:nobollel:20161103100411p:plain