Unityで忍者アプリを作ってみた

さて、Unityで忍者アプリを作ってみました。 短期開発ですが、今回は欧米狙いの色が強いアプリに仕上げています。

https://itunes.apple.com/app/id891186517?at=10l8JW&ct=hatenablog

Android版はこちら↓

https://play.google.com/store/apps/details?id=com.cflat.ninja

フィールドはアセットを購入して、組み立てました。 同様に忍者も購入。

今回はNGUIも購入しました。 今回NGUIで使ったのはボタン程度ですが、基本的に使いやすいですね。 一つ困ったのは、画面サイズに合わせたボタンレイアウトの調整ですが、 [Unity]NGUIで画面サイズに合わせる(NGUI2.3.0対応版) の神記事に助けてもらいました。 よりデザインにこだわろうとすると、もちろんこれだけでは厳しいですが。

今回何より困ったのは、高速化。 iPhoneだと動作が猛烈に重く、ここに工数がかかりました。 原因はフィールドのアセットにあったのですが、そのせいでDrawcall数がかなり多くなっていた。(150くらいだったか) 「UnityでiPhone向け3Dゲームを作る」レポート - ゲームの花園

最初はアセットのpsdがものすごく重いことが原因だとかいろいろ当たりつけてやったんですが、やっぱりDrawcall数が最も重要。 畳なんかももっとカッチョ良かったのですが、削りまくって削りまくって何とかDrawcall数40くらいまで減らしました。 今回は使えなかったけど、GameObjectをStaticにしてしまうというのは、相当効果が大きい。忍者の動きに合わせてフィールドをどんどん移動させなくてはならんので、Staticにはできないのですが、一時はフィールドをスタティックにして忍者をワープさせようかとも考えたほどです。

あとはスポットライトもなかなかの負荷でした。 パーティクルはほどほどにしないとこれも重さの原因になってしまう。 アセットストアで購入するのも、いろいろ労が伴います。 何とか4Sでもある程度動くようになった。ふうー。 それでもまだ多少重いので、Androidだと機種によってはかなり重いので、人気出そうな感じなら改良かな。

そんなこんなでとにかくリリースしました。 良かったら触ってみてください。

C++からC++らしくJNIを呼んでみる

 JNI(Java Native Interface)JavaとCの連携技術です。Javaアプリケーションの一部ロジックをCで書いたり、さらにその中からJavaのメソッドを呼び出したりする事で、アプリケーションの実行環境が制限される代わりに高速化、あるいは非Javaプラットフォームのコードとの共通化が行なえる、という寸法です。

JavaからC++の関数を呼び出す場合

 JNIを用いてJavaからC++の関数を呼ぶ場合、こんな感じになります:

// Java
package com.cflat;

public class Hoge
{
  static {
    System.loadLibrary("hoge");
  }

  public static native boolean printNative(String s, int x);

  public static boolean printJava(String s, int x)
  {
    System.out.println(s + " " + x);
    return true;
  }
}
// C++
extern "C" {
  JNIEXPORT jboolean JNICALL Java_com_cflat_Hoge_print(JNIEnv *env, jstring s, jint x);
}

 C命名規則で、Java\_<i>packagename</i>\_<i>classname</i>\_<i>methodname</i>()なる関数を作ってやればOK……C++的には嫌な感じの名前ですが、名前マングリングが実装依存になっているC++では、他の言語から関数を呼んで貰おうと思うとextern "C"が必須ですので、まあ仕方ありません。せめてもう少し短い名前をだと言うのであればJNIではなくJNA(Java Native Access)を使うという方法が残されています。

C++からJavaのメソッドを呼び出す場合。

 次に、今度はC++からJavaを呼び出してみます。  まず、C++側ではJavaVMインスタンスに対してGetEnv()を呼び出す事でJNIEnv*を取得しなければならないのですが、このJNIEnv*の値はスレッドごとに変わってしまいます。  しかも、C++側で作られたスレッドには(当然ながら)最初はJNIEnv*が作られていませんので、まずはAttachCurrentThread()しなければなりません。もちろん、スレッド破棄時にはDetachCurrentThread()が必要です。

 ただ、その辺は今回したい話とは少しずれて来ますので省略して、既にJNIEnv*を得たところから始めたいと思います。  そして……実際にやってみると、次のようにイケてない部分ばかりの代物になってしまいます:

// C++
bool printJava(JNIEnv *env, const std::string &s, int32_t x)
{
  jclass klass = env->FindClass("com/cflat/Hoge");
  jmethodID method = env->GetMethodID(klass, "printJava", "(Ljava/jang/String;I)Z"); // ←イケてないその1

  jstring jstr = env->NewStringUTF(s.c_str());

  jboolean result = env->CallStaticBooleanMethod(klass, method, jstr, x); // ←イケてないその2

  env->DeleteLocalRef(jstr); // ←イケてないその3
  env->DeleteLocalRef(klass); // ←┘

  return (result != JNI_FALSE);
}

 JNIはC++でも使えるとは言っても、C用のインターフェースの最低限の部分を#if defined(__cplusplus)#endifで囲っただけのライブラリですから、仕方ない事ではあります。  が、もっとC++らしくするのであれば、せめてこんな形にしたいところでしょう:

// C++
bool printJava(jni::vm &vm, const std::string &s, int32_t x) // jni::vmはJavaVMのラッパー(自動的に適切なJNIEnv*を取得する)
{
  jni::class_info klass = vm.get_class("com/cflat/Hoge"); // jni::class_infoはjclassのラッパー

  auto method = klass.get_static_method<bool(std::string, int32_t)>("printJava"); // シグネチャーは型指定で!
  return method(s, x); // 呼び出しも、get_static_method()に渡した型の通りにoperator()で!

  // jclassやらjstringやらはRAIIで勝手に破棄されるので、わざわざDeleteLocalRef()しなくてOK!
}

 JNIの型のC++らしいラッパーであるjni::vmjni::class_infoは、ある程度C++に慣れれば誰でも作れるはずですが(コピーコンストラクタと代入演算子NewLocalRef()、デストラクタと代入演算子DeleteLocalRef()を忘れないように!)、シグネチャーを型指定で済ますように作るには、かなりC++のテンプレートに慣れていないと難しいのではないでしょうか?
 ものは試し、ちょっと作ってみましょう。

JNIラッパーの構成

 上記のコードを見て気付いた方もいると思いますが、シグネチャーの型指定はJNIの型であるjstringjintではなく、C++の型であるstd::stringint32_tを使っています。何故なら、メソッド呼び出し処理の内部で自動変換してくれる方が扱いやすいので。 場合によっては、同じjstringインスタンスを複数のメソッド呼び出しで共有したい、という場合もあるかもしれませんが(当然、その方が余計なオーバーヘッドが不要になります)、今回は気にしないものとします。必要であればjstringのラッパー←→jstringの変換クラスを追加してやればよいだけでしょう。

 これらを鑑みると、JNIのC++らしいラッパーの実装は、次のような部分から成るようにできるでしょう:

  1. C++型←→JNI型変換部
  2. JNI型←→シグネチャー文字列変換部
  3. JNI型←→CallXXXMethod変換部

 なお、jstring(あるいは他のリファレンス型)をRAIIでいい感じに構築/破棄してくれる部分も1.の中に含まれるものとします。

C++←→JNI型

 まずは、C++型を指定した時に対応するJNI型を求めるためのメタ関数が必要になります。  この部分をどう設計するかは悩ましいのですが、試しに、次のようにしてみましょう:

  1. 1/2/4/8バイトの符号付き整数型は、それぞれjbyte/jshort/jint/jlongに変換する((本当は「int8/16/32/64_tのみ」としたかったところですが、intで指定できないのもなんか嫌なので……))
  2. 4/8バイトの浮動小数型は、それぞれjfloat/jdoubleに変換する
  3. char16_tjcharjcharに変換する((sizeof(wchar_t) == 2の場合、wchar_tjcharにしていいかも))
  4. booljbooleanjbooleanに変換する
  5. template <typename TNative> struct object_converterをユーザーが明示的に特殊化したクラスはjobjectまたは互換性を持つ型に変換する

 4.まではプリミティブ、5.はクラス型ですね。

 ……で、この部分を解説しようと思ったのですが、詳細に解説すると大変な事になるので、いろいろと上手く特殊化済みの次のクラスがあるものとします(コピー、ムーブ、デストラクタは省略してます)。

template <typename TNative>
struct var
{
public:
  typedef TNative native_type;
  typedef 対応するJNI型 jni_type;

  var(JNIEnv *, native_type);
  var(JNIEnv *, jni_type);

  operator native_type() const;
  operator jni_type() const;

private:
  jni_type m_type;
};

 このクラスの特殊ケースとして、インスタンス化のできない次のものを用意します:

template <>
struct var<void>
{
  typedef void native_type;
  typedef void jni_type;

  var() = delete;
};

template <class TNativeRet, class ... TNativeParams>
struct var<TNativeRet(TNativeParams...)>
{
  typedef TNativeRet native_type(TNativeParams...);
  typedef typename var<TNativeRet>::jni_type jni_type(typename var<TNativeParams>::jni_type...);

  var() = delete;
};

 ……これで、関数タイプのシグネチャーに対しても対応するJNIの関数タイプを求められるようになりました。

JNI型←→シグネチャー文字列

 JNIのシグネチャー文字列には、大きく分けて4つのパターンがあります。

  1. 英字1文字(プリミティブ型)
  2. "L"+クラス名+";"(非プリミティブ型)
  3. "["+他の型(配列型)
  4. "("+引数型シグネチャーリスト+")"+戻り値型(関数シグネチャー)

 これを、できればコンパイル時にjintと渡せば"I"、jstringと渡せば"Ljava/lang/String;"、void(jstring, jint)と渡せば"(Ljava/lang/String;I)V"のように文字列を構築できれば良いのですが、問題は2.のクラス名パターン。  というのも、std::stringは基本的にconstexprに対応していませんし、const char*では文字列結合が困難だからです。テンプレートを使うとしても、テンプレートで文字列リテラルを与えられないので、コンパイル時に文字列構築をする事ができません(「文字列変数のアドレス」を指定する事だけは可能ですが、指示先をコンパイル時に参照できないので意味なし)。  こんな時、☆C++11テクニック☆ 配列を配列で初期化する方法+α - 株式会社CFlatの明後日スタイルのブログで紹介した方法を使って、constexprな文字列結合を行なってやればよいでしょう。

 これだけわかっていれば、後は面倒なだけで大した事ないので割愛します。  とりあえずsignature<T>::get()あたりで取れてくるようにすればいいでしょう。

JNI型←→CallXXXMethod

 ここまで来れば、後は完全に作業ですね。まず、次のようなクラスを作り……

template <typename TJniRet>
struct method_caller_info
{
  static_assert(std::is_convertible<TJniRet, jobject>::value, "TJniRet is not a primitive type, void, nor jobject");

  static auto static_method_caller() -> decltype(&JNIEnv::CallStaticObjectMethod) { return &JNIEnv::CallStaticObjectMethod; }
  static auto instance_method_caller() -> decltype(&JNIEnv::CallObjectMethod) { return &JNIEnv::CallObjectMethod; }
  static auto nonvirtual_method_caller() -> decltype(&JNIEnv::CallNonvirtualObjectMethod) { return &JNIEnv::CallNonvirtualObjectMethod; }
};

template <typename TJniRet>
struct method_caller : method_caller_info<TJniRet>
{
  typedef TJniRet result_type;

  template <typename ... TJniParams>
  static result_type call_static(const class_info &klass, jmethodID method_id, TJniParams ... params)
  {
    return (result_type)(klass.jni_env()->*method_caller_info<TJniRet>::static_method_caller())(klass.jni_value(), method_id, params ...);
  }
  template <typename TJObject, typename ... TJniParams>
  static result_type call_instance(const object<TJObject> &obj, jmethodID method_id, TJniParams ... params)
  {
    return (result_type)(obj.jni_env()->*method_caller_info<TJniRet>::instance_method_caller())(obj.jni_value(), method_id, params ...);
  }
  template <typename TJObject, typename ... TJniParams>
  static result_type call_nonvirtual(const object<TJObject> &obj, const class_info &klass, jmethodID method_id, TJniParams ... params)
  {
    if (obj.jni_env() != klass.jni_env()) throw env_mismatch();
    return (result_type)(obj.jni_env()->*method_caller_info<TJniRet>::nonvirtual_method_caller_type())(obj.jni_value(), klass.jni_value(), method_id, params ...);
  }
};

プリミティブ型用に、C++らしくなくて嫌ですが、こういう時はCプリプロセッサマクロにお任せ。

#define DEFINE_PRIMITIVE_METHOD_CALLER(jni_primitive_type, methodName) \
template <> \
struct method_caller_info<jni_primitive_type> \
{ \
  static auto static_method_caller() -> decltype(&JNIEnv::CallStatic##methodName##Method) { return &JNIEnv::CallStatic##methodName##Method; } \
  static auto instance_method_caller() -> decltype(&JNIEnv::Call##methodName##Method) { return &JNIEnv::Call##methodName##Method; } \
  static auto nonvirtual_method_caller() -> decltype(&JNIEnv::CallNonvirtual##methodName##Method) { return &JNIEnv::CallNonvirtual##methodName##Method; } \
};

DEFINE_PRIMITIVE_METHOD_CALLER(void, Void);
DEFINE_PRIMITIVE_METHOD_CALLER(jboolean, Boolean);
DEFINE_PRIMITIVE_METHOD_CALLER(jbyte, Byte);
DEFINE_PRIMITIVE_METHOD_CALLER(jchar, Char);
DEFINE_PRIMITIVE_METHOD_CALLER(jshort, Short);
DEFINE_PRIMITIVE_METHOD_CALLER(jint, Int);
DEFINE_PRIMITIVE_METHOD_CALLER(jlong, Long);
DEFINE_PRIMITIVE_METHOD_CALLER(jfloat, Float);
DEFINE_PRIMITIVE_METHOD_CALLER(jdouble, Double);

 これでまず、戻り値の型を指定して適切なCallXXXMethod()を取得してくれるクラスmethod_callerが作れました。後は、実際に呼び出す処理を作ればよいでしょう。

 例によっていろいろと省略して、staticメソッドクラスだけ実装してみます:

class method_info
{
protected:
  typedef jmethodID (JNIEnv::*get_method_func_type)(jclass klass, const char* name, const char* signature);

  method_info(const class_info &klass, get_method_func_type func, const char *name, const char *signature);

  class_info m_class;
  jmethodID m_method;
};

template <class TRet>
struct static_caller
{
  template <class ... TMethodParams>
  static typename marshal::var<TRet>::native_type call(const class_info &klass, jmethodID method, TMethodParams ... params)
  {
    typedef typename marshal::method_caller<typename marshal::var<TRet>::jni_type> method_caller;

    auto retval = method_caller::call_static(klass, method, params ...);
    return (typename marshal::var<TRet>::native_type)marshal::var<TRet>(klass.jni_env(), retval);
  }
};

template <>
struct static_caller<void>
{
  template <class ... TMethodParams>
  static void call(const class_info &klass, jmethodID method, TMethodParams ... params)
  {
    typedef typename marshal::method_caller<void> method_caller;

    method_caller::call_static(klass, method, params ...);
  }
};

template <class TRet, class ... TParams>
class static_method_info<TRet(TParams...)> : private method_info
{
public :
  static_method_info(const class_info &klass, const char *name)
    : method_info(klass, &JNIEnv::GetStaticMethodID, name, marshal::signature<TRet(TParams...)>())
  {}

  typename marshal::var<TRet>::native_type operator()(const TParams & ... params)
  {
    if (!m_method) throw std::runtime_error("jni: Invalid method is called.");

    return static_caller<TRet>::call(m_class, m_method, (typename marshal::var<TParams>::jni_type)marshal::var<TParams>(m_class.jni_env(), params) ...);
  }
};

 途中のstatic_callerクラスは、戻り値がvoidとそれ以外で、戻り値の型変換をする必要があるかどうかが変わるため。関数テンプレートは部分特殊化ができないため、クラスを作っています。

実際のコード

 実際に、弊社アプリ『Fuse』のAndroid版で、上記の仕組みを使ってみました。  Fuseはなるべく共通のソースコードを用いてiPhone/Android両対応できるよう、Cocos2d-xにて開発されていますので、基本的な部分はC++で開発し、プラットフォーム依存な部分のみをObjective-C++、あるいはJava+JNIで開発しています((一応、Cocos2d-xのAndroid用プロジェクトには、JNIEnv*の取得部分だけを微妙に使い易くしたようなヘルパークラスがあるのですが))。

 Fuseでは、例えばGoogle Analyticsのイベント送信等をC++コードから行なうために、Javaで次のようなクラスを作っています:

package com.cflat;

import com.google.analytics.tracking.android.*;

import android.app.Activity;

public class GoogleAnalyticsTracker
{
  static Activity m_activity = null ;

  public static void sendAppView(String screenName)
  {
    if (m_activity == null) return ;

    EasyTracker tracker = EasyTracker.getInstance(m_activity) ;
    tracker.set(Fields.SCREEN_NAME, screenName);
    tracker.send(MapBuilder.createAppView().build());
  }

  public static void sendEvent(String category, String action, String label, int value)
  {
    if (m_activity == null) return ;

    EasyTracker tracker = EasyTracker.getInstance(m_activity) ;
    tracker.send(MapBuilder.createEvent(category, action, label, Long.valueOf(value)).build()) ;
  }

  public static void initialize(Activity activity)
  {
    m_activity = activity ;
    EasyTracker.getInstance(m_activity).activityStart(m_activity);
  }

  public static void terminate()
  {
    EasyTracker.getInstance(m_activity).activityStop(m_activity);
  }
}

 これを、C++側からは次のような形で呼び出しています:

using namespace cflat::jni;

bool Tracker::sendAppViewImpl(const char *screenName)
{
  auto method = vm::instance().get_class(CLASS_NAME).get_static_method<void(std::string)>("sendAppView") ;
  if (!method) return false ;

  method(screenName) ;

  return true ;
}

bool Tracker::sendEventImpl(const char *category, const char *action, const char *label, int value)
{
  auto method = vm::instance().get_class(CLASS_NAME).get_static_method<void(std::string, std::string, std::string, int)>("sendEvent") ;
  if (!method) return false ;

  method(category, action, label, value) ;

  return true ;
}

 JNIをそのまま使うのと比べ、かなりわかりやすくなっている事がおわかりいただけるかと思います。

cocos2d::CCGLProgramがアレなワケ

 前回のオマケ的な何か。

 前回記事の最後で、私は次のように書きました:

ともあれ、上記の結果から、原則としてfor版を使いつつ、for版が使えない時にだけ非for版を使ってやればよさそうだとわかります。
実はこの部分にもこっそりと妙な罠が潜んでいるのですが、これについてはまた別の記事にて。

 というわけでその「別の記事」が今回です。

cocos2d::CCGLProgramのおかしな仕様

 まず、次のコードを、適当なところに書いてみます。さて、どのようになるでしょうか?
 コードは Cocos2d-x 2系ですが、3系でも同様の事が起こるので試してみて下さい。

CCGLProgram *prog = new CCGLProgram();
bool result = prog->initWithVertexShaderByteArray("a invalid vertex shader.", "a invalid fragment shader.");
CCLog(result ? "success" : "failure");
CC_SAFE_DELETE(prog);

 この時、"a invalid vertex(fragment) shader."というのは明らかに GLSL としては不正な文字列ですから、initWithVertexShaderByteArray()は失敗するでしょう。常人であれば、CCGLProgram自身は未使用のまま削除されるので描画には全く影響を及ぼさず、幾つかのコンパイルエラーメッセージと "failure" が出力されるだけ、と考えるのが普通だろうと思います。

 が……何と、実際は 違います
 かといってもちろん "success" が出力されるわけでもなく、あろう事かプログラムが 強制終了 という憂き目に遭うでしょう。

一体どういうこと?

 この、にわかには信じ難い仕様の原因を、試しに Cocos2d-x のソースコードを読んで探ってみます。

bool CCGLProgram::compileShader(GLuint * shader, GLenum type, const GLchar* source)
{
    GLint status;
    
    //// 中略 ////

    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);

    if (! status)
    {
        //// 中略 ////

#if (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
        return false;
#else
        abort();
#endif
    }
    return (status == GL_TRUE);
}

 ……おわかりでしょうか? この燦然と輝くabort();の文字。
 ! statusという条件は GLSL のコンパイルに失敗した時のものですから、どうやら Cocos2d-x は GLSL のコンパイルに失敗すると問答無用でプログラムを異常終了させてくれるようです。

 普通に考えてありえない。一体、何のために引数がbool型になっているのやら(どうやらsource == nullptrの時にだけfalseみたいですよ)……というわけで皆さんは、Cocos2d-x を導入したら必ず、このabort()は削りましょう。

ライブラリ側を弄りたくないよって人は

 自分だけで開発しているならともかく、複数人で開発している場合、Cocos2d-x のライブラリ側はなるべく弄りたくないでしょう。
 そんな時は、CCGLProgram::compileShader()を参考に、コンパイル可能かどうか調べる関数を作ってやりましょう。

 やる事は簡単です。ほとんどCCGLProgram::compileShader()をコピってくるだけ。

/**
 * GLSLソースコードがコンパイル可能かどうか確認する
 *
 * @param GLenum  type   シェーダのタイプ(GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, ...)
 * @param GLchar* source GLSLソースコード
 */
bool isCompilable(GLenum type, const GLchar *source)
{
  const GLchar *sources[] = {
#if (CC_TARGET_PLATFORM != CC_PLATFORM_WIN32 && CC_TARGET_PLATFORM != CC_PLATFORM_LINUX && CC_TARGET_PLATFORM != CC_PLATFORM_MAC)
    (type == GL_VERTEX_SHADER ? "precision highp float;\n" : "precision mediump float;\n"),
#endif
    "uniform mat4 CC_PMatrix;\n"
    "uniform mat4 CC_MVMatrix;\n"
    "uniform mat4 CC_MVPMatrix;\n"
    "uniform vec4 CC_Time;\n"
    "uniform vec4 CC_SinTime;\n"
    "uniform vec4 CC_CosTime;\n"
    "uniform vec4 CC_Random01;\n"
    "//CC INCLUDES END\n\n",
    source,
  } ;

  GLuint shader = glCreateShader(type);
  glShaderSource(shader, sizeof(sources)/sizeof(*sources), sources, nullptr);
  glCompileShader(shader);
  GLint status ;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
  bool result = (status != GL_FALSE) ;
  glDeleteShader(shader) ;

  return result ;
}

 CCGLProgram::initWithVertexShaderByteArray()にシェーダのソースを渡す前に、一旦この関数で有効性を確認する事にします。もしくは、CCGLProgramを継承したクラスを作って、initWithVertexShaderByteArray()の前に必ず有効性チェックをかける事にしても構いません(ただし、CCGLProgram::initWithVertexShaderByteArray()は non-virtual なので、そこだけ注意)。
 こうしておけば、デバイスごとに最適なシェーダプログラムを走らせる事が容易になるでしょう。

ワールドカップに合わせてUnityでサッカーゲームを作ってみた

AppStoreの審査が開幕のブラジル戦に間に合うのかどうか、これが一番の懸案でしたが、1回リジェクトくらったため間に合いませんでした。コートジボワール戦にも間に合わず、何とかギリシャ戦には間に合ったという惨状です。

さて、会社でUnity Proも購入したし、まずは一つリリースしてみようということで、サッカーゲームを作ってみました。 開発期間はかなり短期間です。

https://itunes.apple.com/app/id885300966?at=10l8JW&ct=hatenablog

Android版はこちら↓

https://play.google.com/store/apps/details?id=com.cflat.ClassicDrivingShot

スタジアムとキーパーはアセットを購入しました。 購入するまで、アニメーションがわからないアセットがあるので、この点ストアを改善してほしいですな。 結局一人目のキーパーは使えないキーパーだったので、二人目を購入しました。 世界中探しても、なかなか理想的なキーパーは見当たらないものです。 あと、サウンドも購入するまで音がわからないというのは、買う勇気が湧かない。。。 時間もないし、サウンドはフリーのものを使いました。

あとは、フリックでドライブシュートを打てるようにして、キーパーのColliderをちまちま作ってやって、ドライブシュートの軌道からキーパーの飛ぶ方向を計算して、飛ばしてやる。 ドライブは、フリックダウンの時間に比例して力を加えています。 距離を使うと、パラメータ調整がかなり難しくなりそうだったので。 ゴールが決まった場所に応じて、1ゴール、2ゴール、4ゴール、8ゴール、16ゴールまで獲得でき、これが一つのインセンティブです。 16ゴールは基本的に幻のゴールで、上隅の限られた領域に限定されます。開発者も3回くらいしか出したことない。

GUIは評判通り、開発しにくい。 NGUI買うべきなんでしょうが、今回はひとまずフリーでやりました。 次は購入かなあ。

あとは実機デバッグがやっぱり問題ですな。 Unity Remoteは素晴らしく、ドライブシュートのフリック部分の開発はこれでかなり助けられました。 が、画質も悪く、GUI周りの確認とか、結局実機出力して確認という作業を何回もやりました。 そんでもってネイティブ出力がとても遅い。

タイトル画像がダサいけど、ダサいからアンインストールするって人はほとんどいないだろうし、他にもいろいろ導入したいものもあったけど、タイムリミットが来たので、ひとまずリリースして、その辺はおいおい。。。 マルチプラットフォームの素晴らしさも享受できたけど、元が取れるかな。

早速、レビューも書いていただきました。 フライングドライブシュートを作ったつもりはなかった。

空前のパズルブームを予想してCocos2dでパズルゲームを作ってみた

 この度、弊社よりパズルゲーム『Fuse 〜 導火線パズルドカーン! 〜』(以下『Fuse』)がリリースされました。

 開発はCocos2d-x 2.2.2で行ないました(今までちょこちょことCocos2d-x関連の記事があったのは、これのための研究のせい*1)。  本当は3.0系を使おうと思ったのですが、その時はまだalpha版だったのでやめておきました……C++11で多少使い易くなっただけで、本質的な機能改善はあんまりなさそうでしたし。

 さて、Fuseの話に戻りましょう。  ルールは簡単、画面の上の方に並んでいる導火線パネルを下に置いて、爆弾と爆弾の間を導火線パネルで繋いで爆破するだけ!

f:id:cflat-inc:20140623094351p:plain

 ステージ数は全60+隠し1。クリア時にTwitterFacebook・LINE等に投稿できる仕組みを備えています。  この辺りの仕組みを、iOSでもAndroidでもなるべくCocos2d-xから共通で呼び出せるよう、一部、Objective-C++やJava+JNIにて開発。JNI部分については後日、別途記事にしてみたいと思います。

*1:結局、Android例のGLSLを走らせるのは不安要素が大きすぎたので、結局断念したのですが……。

続・Cocos2d-xの輪郭線とか影回りを実用的に魔改造してみた

 以前の記事に引き続き。

 以前、「輪郭線が iOSAndroid にしか対応していなくて云々」というような理由で GLSL を用いた輪郭線表示を行なうようにしましたが、これを Android で実行してみると機種によってはこうなります。

error C5013: profile does not support "for" statements and "for" could not be unrolled.

 つまり、GPU だかドライバだかが GLSL のforループ(もちろんwhileと、ついでにswitchにも)には対応していない、というエラーです。はて困りました。

 こんな時、どうすればいいかは簡単です。
 forループが使えないなら、 ループを展開した GLSL を動的生成しちゃえばいいじゃない

というわけでループを展開してみた……ところ

 以前の記事でループを使っていた部分は2ヵ所。影を複数つけられるようにしたので影ごとにループを回す部分と、それぞれのピクセルが影のぼかし部分や輪郭線の部分に入っていないか、周囲のテクスチャのピクセルを探す部分です。
 前者は最大数がkCCDecorationLabelMaxShadowCountで決められていたので簡単ですが、後者が困難。というのも、影の大きさ次第でループ範囲が変わるので、ループ範囲を決め打ちできないのです。
 なら、描画する度に影のサイズをチェックして、影が前回よりも大きくなっていたらループ範囲を増やせばよいのですが、実際にやってみると極めて重大な問題が持ち上がりました。シェーダの実行が、恐ろしく重いのです……具体的にはテクスチャ1枚に分単位で時間がかかります。

 理由を調べていったところ、どうやら GLSL のif文は、条件を満たすかどうかにかかわらず、常に分岐後の式を評価して、後で結果を捨てる模様。そりゃあ不要な処理が大量に増えるわけで、遅くなるのも当たり前です。
 なので、折角コードを動的生成するのだから、全ての影についてなるべく必要最低限のみを使うようにしてしまいましょう。少々面倒ですが、毎回影の数とサイズをチェックして、一度使った事のあるパターンであればキャッシュしたプログラムを使い、そうでなければ新たにプログラムを生成してキャッシュに突っ込む感じです。

例えばこんな感じに

 以前と比べると、uniform変数の種類や意味が変わっていますが、説明は割愛します。今回の記事とは無関係なところではありますが、除算よりも高速な乗算を使うため、texGeom.xyを逆数で入れるようにしてあるところは注意です。  あと、なんかよくわからなかったαが2乗される問題がまだ解決できていないため、gl_FragColor = color;ではなくgl_FragColor = vec4(color.rgb, sqrt(color.a));になっています。

メインパーツ

 まずは、メインとなるGLSLパーツ。

uniform sampler2D texture; /* ベース・テクスチャ */
uniform vec4 tintColor;    /* 文字色 */
uniform vec4 texGeom;      /* vec4(1/texture.width, 1/texture.height, texture.x, texture.y) */
uniform vec4 shadowOuterColors[$OUTER_SHADOW_COUNT$]; /* 外側の影の色 */
uniform vec4 shadowOuterShapes[$OUTER_SHADOW_COUNT$]; /* 外側の影の形状 vec4(dx, dy, extent, blur) */
uniform vec4 shadowInnerColors[$INNER_SHADOW_COUNT$]; /* 内側の影の色 */
uniform vec4 shadowInnerShapes[$INNER_SHADOW_COUNT$]; /* 内側の影の形状 vec4(dx, dy, extent, blur) */

/* ベース・テクスチャのアルファを求める。アンチエイリアスのかかっている場所では 0 と 1 の間を取る */
float baseAlpha(vec2 coord)
{
  return texture2D(texture, coord * texGeom.xy).a;
}

/* (-∞, edge0] の区間で 0 、(edge0, edge1) の区間で 0〜1 、[edge1, +∞) の区間で 1 となるような関数 */
float linearstep(float edge0, float edge1, float x)
{
  return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); /* 使用法的に、常にedge0<edge1の保証有り */
}

/* 下の色と上の色をアルファつきで混ぜる */
vec4 mergeOpaqueColors(vec4 under, vec4 over)
{
  if (over.a == 0.0) return under; /* 上の色は透明 */

  float under_a = under.a * (-over.a) + under.a;
  float a = over.a + under_a;
  return vec4((over.rgb * over.a + (under.rgb * under_a)) / a, a);
}

/* 文字の外側のピクセルについて、文字輪郭までの距離を求める */
float getOuterDist(vec2 pos, float rMin, float len)
{
  float a = baseAlpha(pos);
  /* 以下、if (a != 0) return min(rMin, len - a); else return rMin; を高速化したもの */
  return mix(rMin, min(rMin, len - a), step(0.0001, a)); /* 0 < 0.0001 < 1/255 */
}

/* 文字の内側のピクセルについて、文字輪郭までの距離を求める */
float getInnerDist(vec2 pos, float rMin, float len)
{
  float a = 1.0 - baseAlpha(pos); /* 内側ならば、アルファを反転 */
  /* 以下、if (a != 0) return min(rMin, len - a); else return rMin; を高速化したもの */
  return mix(rMin, min(rMin, len - a), step(0.0001, a)); /* 0 < 0.0001 < 1/255 */
}

$OUTER_SHADOW_FUNCS$

$INNER_SHADOW_FUNCS$

void main(void)
{
  /* 外側の影を描画 */
  vec4 color = vec4(0.0, 0.0, 0.0, 0.0);
$DRAW_OUTER$
  /* フォント色で描画 */
  float face_a = baseAlpha(gl_FragCoord.xy - texGeom.zw);
  if (0.0 != face_a) {
    vec4 innerColor = tintColor;

    /* 文字の内部なので、内側の影を描画 */
$DRAW_INNER$
    /* アンチエイリアス分を考慮しつつ、外側の影とフォント色を合成 */
    color = mergeOpaqueColors(color, vec4(innerColor.rgb, innerColor.a * face_a));
  }

  gl_FragColor = color;
}

 ここで、$〜$で囲まれた部分は、動的生成のためのプレースホルダです。$〜$でなくとも、好きなようにプレースホルダを作ればよいのですが。
 それぞれ、

  • $OUTER_SHADOW_COUNT$: 文字の外側に発生させる影の数
  • $INNER_SHADOW_COUNT$: 文字の内側に発生させる影の数
  • $OUTER_SHADOW_FUNCS$: 動的生成される、外側の影の計算用の関数
  • $INNER_SHADOW_FUNCS$: 動的生成される、内側の影の計算用の関数
  • $DRAW_OUTER$: 動的生成される、外側の影色の合成処理
  • $DRAW_INNER$: 動的生成される、内側の影色の合成処理

を意味します。$OUTER_SHADOW_COUNT$$INNER_SHADOW_COUNT$は自明なので、重要なのはその他4つです。

影の合成処理

 次に、$DRAW_OUTER$$DRAW_INNER$を見てゆきましょう。
 と言っても、実のところ大したことはしていなかったり。

$DRAW_OUTER$

  color = mergeOuterShadow$i$(color); /* $i$番目の外側の影 */

$DRAW_INNER$

    innerColor = mergeInnerShadow$i$(innerColor); /* $i$番目の内側の影 */

 ……はい。本当に全く大したことありませんね。これを外側/内側の影の数だけ繰り返しながら、それぞれについて$i$を影のインデックスに書き換えてやればいい話です。具体的には、例えばこんな形に埋め込まれるわけですね。

void main(void)
{
  /* 外側の影を描画 */
  vec4 color = vec4(0.0, 0.0, 0.0, 0.0);
  color = mergeOuterShadow0(color); /* 0番目の外側の影 */
  color = mergeOuterShadow1(color); /* 1番目の外側の影 */

  /* フォント色で描画 */
  float face_a = baseAlpha(gl_FragCoord.xy - texGeom.zw);
  if (0.0 != face_a) {
    vec4 innerColor = tintColor;

    /* 文字の内部なので、内側の影を描画 */
    innerColor = mergeInnerShadow0(innerColor); /* 0番目の内側の影 */

    /* アンチエイリアス分を考慮しつつ、外側の影とフォント色を合成 */
    color = mergeOpaqueColors(color, vec4(innerColor.rgb, innerColor.a * face_a));
  }

  gl_FragColor = vec4(color.rgb, sqrt(color.a));
}

 そして、このmergeOuterShadow$i$()mergeInnerShadow$i$()をどう動的生成してやるか、というのが今回の話のキモになります。

影の計算関数の動的生成

 関数としては Inner/Outer で別れていますが、やっている事はほぼ同じですので、テンプレートは共通の1種類で済みます。
 プレースホルダ$INNER_OUTER$に、"Inner" または "Outer" が入るイメージ。$CHECK_AROUND_i$が2つめのループである、周囲の影を取得してくる部分です。$CHECK_AROUND_i$は関数ごとに別々の内容が入ります。

/* $i$番目の影の色を追加する */
vec4 merge$INNER_OUTER$Shadow$i$(vec4 lastColor)
{
  vec4 shape = shadow$INNER_OUTER$Shapes[$i$];

  /* 参照するベース・テクスチャ上の位置 */
  vec2 center = gl_FragCoord.xy - texGeom.zw - shape.xy;

  float rMin = shape.z; /* rMin=文字との最短距離 */
  /* 太くした部分の範囲内かどうかを確認するために、centerの周辺のテクスチャ上のピクセルを確認 */
$CHECK_AROUND_i$

  if (shape.z <= rMin) return lastColor; /* 影の外側 */

  /* ぼやけを考慮した影の濃さを求める */
  vec4 col = vec4(shadow$INNER_OUTER$Colors[$i$].rgb, shadow$INNER_OUTER$Colors[$i$].a * linearstep(-shape.z, 1.0 - shape.w, -rMin)); /* linearstep(-a, -b, -x) == 1.0 - linearstep(b, a, x) */

  /* 現在色と合成 */
  return mergeOpaqueColors(lastColor, col);
}

 $CHECK_AROUND_i$のテンプレートは、こんな感じです:

  rMin = get$INNER_OUTER$Dist(center + vec2($x$.0, $y$.0), rMin, $length$);

 ここで、$x$, $y$は、半径=shadow$INNER_OUTER$Shapes[$i$].zの円の内部に入る全てのピクセル$length$は中心からそのピクセルの間の距離となります(length(vec2($x$, $y$))と一致)。
 最終的に、merge$INNER_OUTER$Shadow$i$は例えば次のような形に実体化される事になります。

/* 0番目の影の色を追加する */
vec4 mergeOuterShadow0(vec4 lastColor)
{
  vec4 shape = shadowOuterShapes[0];

  /* 参照するベース・テクスチャ上の位置 */
  vec2 center = gl_FragCoord.xy - texGeom.zw - shape.xy;

  float rMin = shape.z; /* rMin=文字との最短距離 */
  /* 太くした部分の範囲内かどうかを確認するために、centerの周辺のテクスチャ上のピクセルを確認 */
  rMin = getOuterDist(center + vec2(0.0, -1.0), rMin, 1.000000);
  rMin = getOuterDist(center + vec2(-1.0, 0.0), rMin, 1.000000);
  rMin = getOuterDist(center + vec2(0.0, 0.0), rMin, 0.000000);
  rMin = getOuterDist(center + vec2(1.0, 0.0), rMin, 1.000000);
  rMin = getOuterDist(center + vec2(0.0, 1.0), rMin, 1.000000);

  if (shape.z <= rMin) return lastColor; /* 影の外側 */

  /* ぼやけを考慮した影の濃さを求める */
  vec4 col = vec4(shadowOuterColors[0].rgb, shadowOuterColors[0].a * linearstep(-shape.z, 1.0 - shape.w, -rMin)); /* linearstep(-a, -b, -x) == 1.0 - linearstep(b, a, x) */

  /* 現在色と合成 */
  return mergeOpaqueColors(lastColor, col);
}

実行速度の比較

 かなり大きめの影を取って以前のコード(をもう少し改良して速くしたもの)と比べると、次のような差がありました。

Device 旧コード 今回コード
iPod touch(第5世代) 970ms 14706ms
Nexus 7 GLSLコンパイルエラー 1360ms
Galaxy SII(SC-02C)※フォント小 0ms 0ms
Galaxy SII(SC-02C)※フォント大 0ms(表示崩れ) アプリ強制終了

 ……Galaxy SII の当てにならなさが酷い。表示崩れの時はGL_OUT_OF_MEMORY​が返ってきてるので、どうやらテクスチャメモリ確保に失敗している模様です。フォントと影の大きさが大きすぎたんでしょうね。それにしても、確保失敗したのにそのままnullptrも返さず処理を続行するcocos2d::CCRenderTexture::create()さんが漢らしすぎる。他のcreate()系関数のnullptr返す仕様と合わさってればいいのに。

 ともあれ、上記の結果から、原則としてfor版を使いつつ、for版が使えない時にだけ非for版を使ってやればよさそうだとわかります。
 実はこの部分にもこっそりと妙な罠が潜んでいるのですが、これについてはまた別の記事にて。

この本、おすすめです。

☆C++11テクニック☆ 配列を配列で初期化する方法+α

メンバー変数の初期化

 例えば、こんなコードがあったとします。

template <class T>
struct Hoge
{
  Hoge(const T &v) : value(v) {}

  T value;
};

template <class T>
Hoge<T> make_hoge(const T &v)
{
  return Hoge<T>(v);
}

#include <iostream>
#include <cxxabi.h>
#include <cstdlib>
#include <memory>

template <class T>
std::ostream &print(std::ostream &os, const T &value)
{
  int status;
  std::unique_ptr<char, decltype(&std::free)> demangled(abi::__cxa_demangle(typeid(value).name(), 0, 0, &status), &std::free);
  return os << demangled.get() << ": " << value << std::endl;
}

int main()
{
  auto hoge = make_hoge(142857);

  print(std::cout, hoge.value);

  return 0;
}

 結果はもちろん、こうなります。

int: 142857

メンバー変数が配列になると……?

 では次に、make_hoge()に渡す実引数を次のように変えてみましょう。

  constexpr auto hoge = make_hoge("142857");

 コンパイルすると、次のようなエラーが発生します。

main.cpp: In instantiation of 'Hoge<T>::Hoge(const T&) [with T = char [7]]':
main.cpp:12:19:   required from 'Hoge<T> make_hoge(const T&) [with T = char [7]]'
main.cpp:30:29:   required from here
main.cpp:4:29: error: array used as initializer
   Hoge(const T &v) : value(v) {}

 どうやら、「配列をvalue(v)の形式で初期化できないよ」と言われているようです。  仕方ないので、配列の時だけコンストラクタを特殊化して、次のようにしてみましょう:

template <class T, int N>
struct Hoge<T[N]>
{
  Hoge(const T (&v)[N]) { std::copy_n(&v[0], N, &value[0]); }

  T value[N];
};

 出力は、こうです。

char [7]: 142857

 おお、上手く行きました!  では次は、このクラスをコンパイル時に構築できるよう、Hogeのコンストラクタやmake_hoge()constexprをつけてみましょう。  コンパイル結果は、こうなります:

main.cpp: In constructor 'constexpr Hoge<T [N]>::Hoge(const T (&)[N])':
main.cpp:14:71: error: constexpr constructor does not have empty body
   constexpr Hoge(const T (&v)[N]) { std::copy_n(&v[0], N, &value[0]); }
                                                                       ^
main.cpp: In function 'int main()':
main.cpp:40:39: error: the type 'const Hoge<char [7]>' of constexpr variable 'hoge' is not literal
   constexpr auto hoge = make_hoge("142857");
                                       ^
main.cpp:12:8: note: 'Hoge<char [7]>' is not literal because:
 struct Hoge<T[N]>
        ^
main.cpp:12:8: note:   'Hoge<char [7]>' is not an aggregate, does not have a trivial default constructor, and has no constexpr constructor that is not a copy or move constructor

 どうやら、constexprなコンストラクタに中身がある事がお気に召さない様子。C++14では(std::copy_n()constexprでさえあれば)大丈夫なのかもしれませんが、少なくともgcc4.9.0では-std=c++1yをつけてやってもダメです。  では、どうすればよいのでしょうか?

 さて、ここでC++の配列初期化方法を思い出してみます。

int hoge[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

 つまり、この方法を利用すれば、コンストラクタの中身を空にする事ができるのでは?  ……はい、その通りです。

template <class T, int N>
struct Hoge<T[N]>
{
  constexpr Hoge(const T (&v)[N]) : value{/* 0, 1, 2, ... */} {}

  T value[N];
};

 それではこのvalue{}の中身は、どう書けばよいのでしょうか……というのが今回の本題になります。ああ長かった。

N要素の配列メンバー変数の初期化

 配列の要素数ごとにvalueの初期化子の要素数が変わるので、任意のNに対して適切な初期化子を書くのは非常に困難なように見えます。  C++メタプログラミングを始めたばかりの人であれば、最初に思いつくのはこんなコードでしょう:

template <class T>
struct Hoge<T[1]>
{
  constexpr Hoge(const T (&v)[1]) : value{v[0]} {}

  T value[1];
};

template <class T>
struct Hoge<T[2]>
{
  constexpr Hoge(const T (&v)[2]) : value{v[0], v[1]} {}

  T value[2];
};

template <class T>
struct Hoge<T[3]>
{
  constexpr Hoge(const T (&v)[3]) : value{v[0], v[1], v[2]} {}

  T value[3];
};

 利用する全てのNについて、コンストラクタを特殊化してしまえばよろしい。解決ですな。

 ……もちろん、そんな頭の悪い事など推奨できるはずもありません。だってこれ、#defineすらできないじゃん。できてもやりませんが。  C++11erであれば、当然次のように書くでしょう:

template <class T, int N>
struct Hoge<T[N]>
{
  template <int ... i>
  constexpr Hoge(const T (&v)[N]) : value{v[i] ...} {}

  T value[N];
};

 さて問題は、このint ... iをどのように生成するか、になります。  もちろん、Hoge<T[N]><0, 1, 2>("10")なんて事を手動で書きたくはありません。iの数を間違えると、余裕で配列外にアクセスしてしまいますし。

Nから0...N-1を作る方法

 こんな時、Nから0...N-1のリストを作れるようにすれば、物事は万事解決です。  ちょっとだけクラス数は増えますが、こんな感じになればよいでしょうか?

template <int N>
class make_indices
{
public:
  template <int ... i> struct indices {};
  typedef indices<i ...> type; // ←ココ
};

template <class T, int N>
class Hoge_impl
{
  friend class Hoge<T[N]>; // コンストラクタのみHoge<T[N]>に公開する

private:
  template <int ... i>
  constexpr Hoge_impl(const T (&v)[N], indices<i...>) : value{v[i] ...} {}

public:
  T value[N];
};

template <class T, int N>
struct Hoge<T[N]> : public Hoge_impl<T, N>
{
  constexpr Hoge(const T (&v)[N]) : Hoge_impl<T, N>(v, typename make_indices<N>::type()) {}
};

 この、make_indices<N>::typeを生成する方法さえ見つかれば、全ては解決です。

 アイディアとしては、テンプレートの再帰を使います。例えばこんな感じ:

template <int N>
class make_indices
{
private:
  // 最初はM==Nで、Mを1つ減らすごとにsizeof...(i)を1増やす
  template <int M, int ... i>
  struct make_indices_impl : public make_indices_impl<M - 1, i ... , sizeof...(i)> {} ;

  // M==0となったので、i...には「0〜N-1」のN連続の値が入っている
  // この時のindices<i...>が、求めるindices<i...>
  template <int ... i>
  struct make_indices_impl<0, i...>
  {
    typedef indices<i ...> type ;
  } ;

public:
  typedef typename make_indices_impl<N>::type type;
};

 実行してみると、

char [7]: 142857

 この通り、予期した通りに動作する事がわかります。

オマケ:今のの応用

 これを応用すると、配列をコンパイル時に逆転させてみたり、

template <class T, int N>
class Hoge_impl
{
  template <int M, int ... i1, int ... i2>
  constexpr Hoge_impl(const T (&v1)[M], indices<i1...>, const T (&v2)[N-M], indices<i2...>) : value{v1[i1] ..., v2[i2] ...} {}

public:
  T value[N];
};

のようにすると配列をコンパイル時に結合してみたりと、テンプレートメタプログラミングの幅が広がります。  もっとも、そこまで高度なメタプログラミングをしなければならないのなら、わざわざ車輪を再発明しなくても素直にSproutライブラリなりCELなりを使った方がずっと良いと思いますが。