エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

iOS

UnityアプリのiPhoneX対応

UnityアプリはXcode9でビルドした場合は互換モードでは動作せず、全画面モードで動作します。Unity 5.5など、古いバージョンのUnityでビルドした場合でも同様です。そのため、縦画面想定で、Canvas Scalerでmatch=1.0などと設定していると、UIの左右が見切れることになります。

この問題を解決するには、画面のアスペクト比を見て、match=0.0に設定する必要があります。具体的に、以下のようなスクリプトを全てのCanvas Scalerを含むGameObjectにアタッチします。尚、Canvas Scalerの初期化よりも前に走らせるために、Project SettingのScript Execution OrderでCanvasScreenAutoFixの実行順を早くしておきます。

//iPhoneXの縦長画面に対応
//Canvasにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CanvasScreenAutoFix : MonoBehaviour
{
	private void Awake()
	{
		if(1.0f*Screen.width/Screen.height<9/16.0f){
			GetComponent<CanvasScaler>().matchWidthOrHeight=0.0f;
		}else{
			GetComponent<CanvasScaler>().matchWidthOrHeight=1.0f;
		}
	}
}

スクリプトのアタッチを手動で行うと大変なので、Editor Scriptで自動化します。

//iPhoneX対応のため、全てのCanvasにCanvasScreenAutoFixをアタッチ

using UnityEngine;
using UnityEditor;
using UnityEngine.Networking;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

public class iPhoneXSupport : MonoBehaviour {
    public static List<T> FindObjectsOfTypeAll<T>()
    {
        List<T> results = new List<T>();
        var s = SceneManager.GetActiveScene();
        if (s.isLoaded)
        {
            var allGameObjects = s.GetRootGameObjects();
            for (int j = 0; j < allGameObjects.Length; j++)
            {
                var go = allGameObjects[j];
                results.AddRange(go.GetComponentsInChildren<T>(true));
            }
        }
        return results;
    }

    [MenuItem ("iPhoneX/Add canvas scaler")]
    static void ChangeToProduction() {
        GameObject[] allObjects = (GameObject[])FindObjectsOfTypeAll( typeof(GameObject) );
        foreach ( GameObject obj in allObjects ){
            if(obj.GetComponent<Canvas>()!=null){
                obj.AddComponent<CanvasScreenAutoFix>();
                Debug.Log(obj.name);
                EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
            }
        }
    }
}

UIは互換表示のように黒帯を表示し、メインゲームは全画面で動作させる場合、UIのRectTransformのAnchorがCenter以外だとレイアウトが崩れる場合があるため、手動で全てCenterに書き換えます。

また、カメラの画角も変わり、大きく表示されるため、補正します。

//iPhoneXの縦長画面に対応
//Cameraにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraScreenAutoFix : MonoBehaviour {

	// Use this for initialization
	void Awake () {
		float initial_field_of_view=GetComponent<Camera>().fieldOfView;
		float ratio=(9.0f/16.0f)/(1.0f*Screen.width/Screen.height);
		if(ratio<1.0f){
			ratio=1.0f;
		}
		GetComponent<Camera>().fieldOfView=initial_field_of_view*ratio;
	}
}

中央寄せではなく、上寄せしているAnchorを持つUIに対しては、セーフエリアの44pxを加算する必要があるため、以下のスクリプトを当てます。

//上寄せのUIのセーフエリアを適用

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SafeArea : MonoBehaviour {
	private void Awake()
	{
		if(1.0f*Screen.width/Screen.height<9/19.0f){
			Transform target=transform;
			while(target!=null){
				if(target.GetComponent<CanvasScaler>()!=null){
					break;
				}
				target=target.parent;
			}

			CanvasScaler canvas_scaler=target.GetComponent<CanvasScaler>();
			float safe_area_y=44.0f;
			safe_area_y=safe_area_y*canvas_scaler.referenceResolution.y/Screen.height;
			Vector3 pos=GetComponent<RectTransform>().anchoredPosition;
			pos.y=pos.y-safe_area_y;
			GetComponent<RectTransform>().anchoredPosition=pos;
		}
	}
}

新規開発のアプリからは、20:9でUIの背景を作成すると共に、端寄せでUIを配置する必要があるかなと思います。

iOSアプリのiPhoneX対応

iPhoneXのノッチに対応するため、StoryboardにSafe Areaが追加されました。従来は、Viewを基準にConstraintsを指定していましたが、Xcode9以降はSafe Areaを基準にConstraintsを指定することになります。

Safe Areaを使用するには、Safe Area Relative Marginsのチェックボックスを有効にします。その後、ConstraintsのAlign Top to:をViewからSafe Areaに変更します。

storyboard

また、Xcode8までは、ステータスバーの背景色を設定することができないため、ダークモードでは以下のように背景色となるビューを追加していました。

var h : CGFloat=20.0
dark_view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: w, height: h))
dark_view!.backgroundColor=bg_color
self.window!.rootViewController!.view.addSubview(dark_view!)

しかし、iPhoneXではステータスバーの高さが44pxに上がっているため、以下のようにサイズを調整する必要があります。

var h : CGFloat=20.0
if #available(iOS 11.0, *) {
    if(UIApplication.shared.windows[0].safeAreaInsets != UIEdgeInsets.zero){
        h=44.0
    }
}

ただ、実際にやってみると、ポートレートモードでは問題ないのですが、iPhoneXのランドスケープモードではステータスバーが表示されなくなったため、追加したサブビューがUIの上に被ってしまいます。そのため、サブビューを追加する方法ではなく、ステータスバーの背景色を直接、変えるようにした方がよいようです。(参考:Changing the Status Bar Color for specific ViewControllers using Swift in iOS8

extension UIApplication {
    class var statusBarBackgroundColor: UIColor? {
        get {
            return (shared.value(forKey: "statusBar") as? UIView)?.backgroundColor
        } set {
            (shared.value(forKey: "statusBar") as? UIView)?.backgroundColor = newValue
        }
    }
}

UIApplication.statusBarBackgroundColor = bg_color;

また、ランドスケープモード時には左右のunsafeエリアが白で塗りつぶされます。この色を変えるには、viewDidLoadでview.backgroundColorに色を設定します。(参考:Changing the background color of the view, Swift

override func viewDidLoad() {
    super.viewDidLoad()
    if(dark_mode){
        let bg_color:UIColor=UIColor(red: r/255, green: r/255, blue: r/255, alpha: 1.0)
        view.backgroundColor = bg_color
    }
}

Xcodeでdouble freeを検出する

XcodeのProduct -> SchemeからAddress Sanitizerにチェックを入れると、mallocで確保したメモリのdouble freeを検出することができます。スタック破損チェックなどに便利です。

sanitizer

Swift3のNSAttributedStringでunrecognized selector例外が起きる

Swift2からコンバートしたSwift3のNSAttributedStringでunrecognized selector例外が発生します。

let attributedOptions : [String: AnyObject] = [
    NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType as AnyObject,
    NSCharacterEncodingDocumentAttribute: String.Encoding.utf8 as AnyObject
]
attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)


String.Encoding.utf8だとダメで、String.Encoding.utf8.rawValueにする必要があるようです。

let attributedOptions : [String: AnyObject] = [
    NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType as AnyObject,
    NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue as AnyObject
]
attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)


参考:Swift-3 error: '-[_SwiftValue unsignedIntegerValue]: unrecognized selector

Xcode7のStoryboardでUITableViewの横幅を100%にする

Xcode7でTabbedApplicationを作成し、FirstViewにUITableViewを配置しただけでは、横幅に600pxが固定値で代入されてしまいます。そのため、iPhone5Sなど、小さな端末ではコントロールがウィンドウサイズを超えてしまいます。

コントロールの横幅をウィンドウサイズに制約するには、Constraintを使います。まず、UITableViewを選択し、CTRLを押しながら上位のViewにドラッグします。


width

出てきたウインドウから、trailing space to container marginを設定すると右端がウィンドウに吸引します。また、loading space to container marginを設定すると、左端がウィンドウに吸引します。

ios 8 Storyboard Search Bar Too Wide

Xcode7におけるリンクエラーへの対策

Xcode7において、Xcode6で作成したStatic Link Libraryをリンクしようとすると、以下の二種類のエラーが発生します。

Enable Bitcodeに関するエラー


Xcode7では、-fembed-bitcodeがデフォルトで有効になりました。そのため、Xcode6でビルドしたStatic Link Libraryをリンクしようとすると、以下のようなリンクエラーが発生します。

ld: '*.a' does not contain bitcode. You must rebuild it with bitcode enabled
(Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor,
or disable bitcode for this target. for architecture arm64


この問題を解決するには、Static Link Libraryに-fembed-bitcodeオプションを付けてビルドする必要があります。サードパーティのライブラリを使用していてリビルドできない場合は、Build OptionsのEnable BitcodeをNOにすることで回避することができます。Unity5.1から生成されるXcodeプロジェクトも同様のリンクエラーが発生しますが、Enable BitcodeをNOに設定することで回避することができます。

ただし、watchOS向けにビルドする場合は、Enable Bitcodeは必須です。Enable Bitcodeを有効にすると、バイナリサイズは概ね3倍に膨らみます。

Enable Bitcodeを有効にすると、生成バイナリにLLVMのビットコードが含まれるようになります。将来的に、新しいCPUアーキテクチャが追加された場合に、App StoreがLLVMのビットコードから自動的にリビルドしてくれるようになります。

参照:Xcode7GMでビルドすると「does not contain bitcode.」とか言われる

Universal Binaryに関するエラー


Xcode7でUniversal Binaryを使用して、シミュレータ向けにビルドしようとすると、以下のようなリンクエラーが発生します。

ld: in *.a, building for iOS simulator,  
but linking in object file built for OSX, for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)


Xcode6のStatic Link Libraryでは、iOS simulator向けのビルドにおいて、-mmacosx-version-min=XX が設定されていますが、Xcode7では-miphoneos-version-min=XX が設定されるようになりました。そのため、Xcode6でビルドした場合は、ターゲットプラットフォームのミスマッチが発生します。

この問題を解決するには、Static Link Libraryに-miphoneos-version-min=XXオプションを付けてビルドする必要があります。サードパーティのライブラリを使用していて、リビルドできない場合の回避方法はありません。

この変更は、armバイナリとx86バイナリがOSX向けかiOS向けかを明確に区別する必要が出てきたためだと考えられます。

参照:Xcode 7's New Linker Rules

Unity5でNative Pluginが動作しない場合に確認すべきこと

Unity4で動作していたNative PluginがUnity5で動作しない場合、以下を確認するとよいです。

・Androidのライブラリ名の確認

Unity4までは、Native Pluginの名前がlibhoge.soだった場合、[DllImport("libhoge")]でも[DllImport("hoge")]でも動作しました。対して、Unity5では[DllImport("hoge")]でないと動作しなくなりました。

・iOSのStatic Link Libraryのビルドオプションの確認

Unity4までは、Static Link LibraryのビルドにGNUのlibstdc++を使う必要がありました。対して、Unity5ではLLVMのlibc++を使用する必要があり、libstdc++ではリンクエラーが発生します。Unity5向けにlibc++を使用するには、Static Link Libaryのビルド時に、clangに-stdlib=libc++を追加してビルドする必要があります。

Unity5自身もUnity製のStatic Link Libraryを使用しているため、Unity5の書き出したXcodeのプロジェクトファイルをlibstdc++を使用するように書き換えてもリンクエラーが発生します。

既存のライブラリのリビルドが必要で、外部ライブラリを使用している場合は、わりと大変かもしれません。ただ、libstdc++に比べて、libc++はC++11対応など近代化が進んでいるので、移行する価値はあるかと思います。

・アドレス空間のオーバフローの確認

Native Pluginへの引数にIntPtr.ToInt32()+hogeとしている場合、64bitアドレスにデータが配置された場合にオーバフローが発生します。IntPtr.ToInt32をIntPtr.ToInt64に変更することで対処します。

Mac OSのバージョンとlibstdc++とlibc++

b2でビルドしたBoost 1.57のStatic Link Libraryのboost::unit_test_frameworkをYosemiteで実行するとSegfaultが起きたので、メインコードのMakefileの-mmacosx-version-min=10.4を-mmacosx-version-min=10.9にすると動作するようになりました。

どうやら、Mac OS 10.6までをターゲットにビルドするとlibstdc++が使用され、Mac OS 10.7以降をターゲットにビルドするとlibc++が使用されるようです。libc++でC++11対応が入ったようですね。

Does homebrew use the option '-c++11' to mean '-stdlib=libc++'?

Xcode6でBoostをビルドすると、デフォルトでlibc++が使用されますが、メインコードで-mmacosx-version-min=10.4を指定するとlibstdc++が使用され、C++の例外周りのABIの互換性の問題か何かで、Segfaultが起きているようです。

リンクまで問題なく通ってしまうので、原因の把握にわりと時間がかかってしまいました。以下のように、メインコードと合わせて、Boostを-mmacosx-version-min=10.4でビルドすると、Segfaultは解消しました。


./b2 clean link=static threading=multi address-model=32_64 --layout=tagged cflags="-mmacosx-version-min=10.4" cxxflags="-mmacosx-version-min=10.4"
./b2 install link=static threading=multi address-model=32_64 --layout=tagged cflags="-mmacosx-version-min=10.4" cxxflags="-mmacosx-version-min=10.4"


また、リンク時は、-mmacosx-version-min=10.4だけだと、libc++とlibstdc++の両方がリンクされてしまうため、-stdlib=libstdc++を追加する必要があるようです。


-mmacosx-version-min=10.4 -stdlib=libstdc++


dylibの依存関係はotool -l hoge.dylibで確認できます。

JavaScriptに追加されたTypedArrayの速度比較

いつの間にかJavaScriptにTypedArrayなるものが追加されていたので速度を比較してみました。今まで、array=new Array(1000)とかしていたのを、array=new Int32Array(1000)とするだけで使えます。

WebGL向けに開発されたということで、普通のArrayよりもパフォーマンスが向上するのでは?という期待感から計測してみました。

計測コードを実行する

一番高速なものを青、特別遅いものを赤で表記しています。単位はmsecです。

.

.

ArrayFloat32ArrayUint8ArrayInt16ArrayInt32Array

.

Chrome19確保047122243

.

変数書き込み251485858692

.

int定数書き込み3153515877

.

float定数書き込み397475257159

.

読み込み4448525994

.

Safari5確保030122445

.

変数書き込み720652820819823

.

int定数書き込み180645801783822

.

float定数書き込み136723103710201008

.

読み込み203896827821815

.

iOS5(NewIpad)確保025142169

.

変数書き込み40502094251324882489

.

int定数書き込み13422107257325562525

.

float定数書き込み8832099299730032978

.

読み込み11074053372237323440

.

Android4確保111101

.

変数書き込み9124701585658405901

.

int定数書き込み167110597659526007

.

float定数書き込み171107731172908430

.

読み込み340186879388489152

.

.



これを見て分かるのは以下の三点です。
・iOSは基本的にArrayを使うのがよい
・AndroidはFloat32Arrayを使うのがよい
・AndroidのIntArray系は50倍遅くて使い物にならない

PC系では以下の二点です。
・ChromeのIntArrayはそんなに悪くない
・SafariのIntArrayは4倍遅いぐらい

JavaScriptのボトルネックは間違いなくメモリアクセスの遅さで、ハッシュではないTypedArrayであれば劇的に高速になってもっと面白いことができるようになる!と思ったのですが、世の中そんなに甘くはありませんでした。速くなるどころか、普通に遅くなります。

iOSでは無難にArrayを使いましょう。特に画像フィルタのようにライトよりもリードの方が多いアプリの場合、4倍くらい速度差が出そうです。ちなみにCanvasのgetPixelDataメソッドの戻り値はUint8Arrayのようですので、場合によってはArrayに変換してもメリットが出るかもしれません。

iOSとAndroidとPCのJavaScriptの処理時間の比較と高速化ノウハウ

2048*2048[px]の画像に対してJavaScriptでフィルタをかけることを想定して、2048*2048*4回の基本演算の所要時間と、2048*2048[px]のCanvasに対するgetImagePixelsとputImagePixelsの速度を計測しました。テストプログラムとテストコードは以下です。

テストプログラム

結果は次の通り。

.

.

計測項目iOS5(NewIpad)Android4(Nexus)Safari5(core i7)Chrome19(core i7)

.

基本演算a++386118482

.

a+b(int)4411044825

.

a+b(int)(|0)6721324925

.

a+b(float)479747825

.

a*b5211039331

.

a/b69339512370

.

a<<b411674943

.

配列array.lengthでループ205234113338

.

array.lengthをローカル変数にコピーしてループ11113538938

.

ループ内で関数呼び出し194935010645

.

CanvasgetImageData51120353167

.

putImageData431955116

.

単位はmsec


ChromeはSafari5よりも基本演算の性能が2倍以上高くなっています。しかし、getImagePixels/putImagePixelsでは、逆にSafari5の方が3倍以上高速です。Chromeはベンチマークでよく使われる基本演算に特化して最適化されており、ベンチマークの少ないCanvas系の最適化はあまり行われていない印象です。

加算と乗算については、Safari5を除いて、速度差はありません。命令のクロック数の差よりも、JavaScriptのコストの差の方が大きいため、乗算を、加算やシフトに置換してもメリットは少なそうです。

floatで演算するよりも、|0をしてintの空間にしてから計算した方が速いという話もありましたが、計測結果を見る限り、|0のコストの方が高いように思われます。

ループ条件の最適化については、AndroidとChromeの場合はfor(var i=0;i<a.length;i++)と書いてもcnt=a.length; for(var i=0;i<cnt;i++)と書いても、速度差は無いようです。しかし、iOSやSafariの場合は、依然として2倍程度の差がありますので、特にiOS向けに開発する場合は、ループ条件のローカル変数へのコピーは必須のようです。また、iOSはループ内での関数呼び出しのコストが、他よりも高いですね。

getImageDataとputImageDataには非対称性があるようです。getImageDataよりもputImageDataの方が10倍も高速です。getImageDataは意外と重いみたいなので、使いすぎに注意ですね。

とりあえずiOSにおける高速化のポイントをまとめると、以下のような感じです。

・配列の.lengthをローカル変数にコピーしてからループすると2倍高速になる
・関数はできるだけインライン展開する
・getImageDataはputImageDataの10倍遅い
・乗算をシフトや加算に置換してもあまり速くならない
・|0でintにキャストして演算すると逆に遅くなる

全体的に、iOSの処理時間は、PCで実装した処理時間の10倍程度遅いと見積もっておけばよさそうです。

AndroidとiOSの開発環境を比較してみた

AndroidとiOSの開発環境を比較してみました。個人的に好きな方に色をつけています。

AndroidiOS解説
開発言語JavaObjectiveCJAVAはリソースの開放をGCに任せられるので楽、iOSは文字列制御だけでも複雑でretainを忘れてメモリリークが発生したりと結構大変
統合開発環境EclipseXcode開発環境は同等ですが、好みでEclipse
エミュレータ遅すぎて使えないかなり優秀Androidのエミュレータは起動だけに数分かかる上にほとんど使えないぐらい重いので実機デバッグが必須、iOSは普通の速度で動いて快適
デバッグ大変容易Androidは機種数が多すぎてメジャーなVDPだけでも4種類あってOSのverもいろいろあるので4機種程度は買わないといけない、iOSは3GSと4で検証すればほぼOK
動作速度遅い速いAndroidはGCとVMがボトルネックで速度があまり出ない、NDKを使えばiOSに近いパフォーマンスを出すことはできるが機種依存が若干不安、iOSはネイティブなので最速
UI設計コードで配置GUIで配置iOSの方がUIは楽に配置できるけどドラッグでオブジェクト間の依存関係をつないだりするのがわかりにくい、Androidは全てプログラムで制御できるのはいいけど狙ったレイアウトにするのがなかなか難しい、ゲームを作る分にはAndroidの方が楽
アプリの配布制約なし制約ありAndroidはapkファイルを配るだけでOK、iOSは事前に登録した機種にしかインストールできない上に3ヶ月ごとにプロファイルを更新してリビルドしなければ開発中のアプリは使えなくなるためアプリを自由に配布することができない、せめて1年にして欲しいです
プラットフォームアプリの開発制約なし制約ありアプリマーケットや電子書籍マーケットなど、プラットフォーム系のアプリはAndroidでは自由に作れますが、iOSでは審査で落とされることが多いです
アプリの売上まだ市場規模が小さい市場規模が大きいiOSの方が3倍程度売上が大きいです


全体的に作りやすいのはAndroidです。初めてのアプリ開発はAndroidがオススメ。ただ、アプリの売上はiOSの方が大きいのが悩ましいところですね。個人的には、Androidでプラットフォーム型のアプリを作るのが将来性があるかなと思ってます。

後、GUI系はWindowsのVisualStudioが圧倒的に作りやすいので、将来的にWindows8とWindowsPhone7が統合されると、結構いいアプリが出てくるんじゃないかなと思っています。

iOS5でOnTouchStartイベントが来ない場合の対処法

iOS4のMobileSafariでは問題無く来るOnTouchStartイベントがiOS5では来ないケースがあったので検証ページを作りました。まとめると、次のようになりました。

iOS4iOS5
divにOnTouchStartを設定OKOK
divにOnMouseDownを設定OKOK
innerHTML内にOnTouchStartを含むdivを動的に作成OKNG
innerHTML内にOnMouseDownを含むdivを動的に作成OKOK

innerHTMLで動的にdivを作る場合で、そのdivにOnTouchStartイベントを登録した場合のみ動作しません。OnMouseDownではOKなのでMobileSafariのバグな気がします。JQueryでもこれが原因でButtonが動作しない問題があるようです。

いろいろ試してみた結果、対処法としては、
 document.getElementById("div_id").innerHTML="<div ontouchstart="alert('hoge');"></div>";
と書かずに
 document.getElementById("div_id").innerHTML="<div id='hoge1'></div>";
 document.getElementById("hoge1").addEventListener("touchstart",function(){alert('hoge');},false);
のように、一度innerHTMLにdivを作った後に、遅延してaddEventListenerをすれば回避できるようです。

AndroidNDKとiOSでライブラリを作る際に不要なシンボルをエクスポートしない方法

AndroidやiOSではライブラリを作ることができます。例えばAndroidではAndroidNDKを使用してC++のソースから.soファイルを作成します。(NDKの使用方法)

NDKでライブラリを作る場合:

コンパイル方法
(1)jniフォルダにC++ソースとmakeファイルを置く
(2)ndk-buildコマンドを実行
(3)libsフォルダに.soが出来る

インタフェース用の.javaを作成
(1)System.loadLibrary("library_name");と記述
(2)C++で記述したものと同じ引数を持つ関数をnative指定で記述


NDKで作ったライブラリを使う場合:

.soと.javaをプロジェクトのlibsフォルダとsrcフォルダにコピー
javaの関数を呼ぶとJava_*というC++の関数が呼ばれる


また、iOSの場合はXcodeの”プロジェクトの新規作成”からStaticLibraryを選択することでスタティックライブラリのプロジェクトを作成することができます。このプロジェクトでコンパイルするとスタティックライブラリである.aファイルが作成されます。ただし、iOSはシミュレータ用にはi386命令、実機用にはarm命令でコンパイルする必要があります。そこで、ビルドターゲットをプロジェクト内で複製し、シミュレータ用とARM用にそれぞれビルド、Lipoで結合してユニバーサルバイナリにします。


lipo -create "DerivedData/test/Build/Products/Release-iphoneos-iphone/test.a" "DerivedData/test/Build/Products/Release-iphonesimulator-simulater/test.a" -output "test.a"


スタティックライブラリを使う場合は、作成したユニバーサルバイナリのtest.aとインタフェースを記述した.hをプロジェクトに追加すればよいです。

しかし、このままだと、Android/iOS共に、関数名や構造体名がだだ漏れになります。試しに.soや.aをテキストエディタで開いてみると、関数名が含まれていることが分かります。このようなライブラリをそのまま配布すると、知られたくない設計情報を解析することが可能になってしまいます。そこで、このような不要なシンボルをエクスポートしない方法を考えます。

Androidの場合は比較的簡単です。gccのビルドオプションに-fvisibility=hiddenを付けると、不要なシンボルはエクスポートされなくなります。エクスポートしたい関数にだけ

 __attribute__((visibility("default")))

を付けます。

 __attribute__((visibility("default"))) JNIEXPORT jint JNICALL Java_abars_test_CreateInstance(JNIEnv *env, jclass obj, jstring filename)

みたいな感じです。

ここまではいいんですが、iOSではこのオプションが使えません。というのも.aはオブジェクトファイルを結合しただけの形式だからです。従って、リンク時に有効な-fvisibilityは無効で、有効な解決策はありません。単純なライブラリであれば、無名名前空間に隠したい関数を全て入れてしまえばよいですが、テンプレートを使っているとうまくいきません。

ということで、結論としてはiOSではシンボルを隠せません。妥当な解決策としては、適当なスクリプトをperlとかで書いて、関数名とクラス名を正規表現で列挙し、ClassNo1とかfunc_no1とか適当なものに置換するぐらいです。

aa3


気合で置換するとこんな感じになります。将来的にはiOSでダイナミックリンクライブラリをサポートして欲しいですね。

iPhoneアプリにTapjoyのリワード広告を導入する方法

1.Tapjoyとは?


Tapjoyはスマートフォンゲーム向けのリワード広告フレームワークです。法人だけでなく個人も使うことができます。Tapjoyのページから会員登録し、SDKをダウンロードして組み込むだけで、リワード広告を導入することができます。

リワード広告は、ユーザがゲームを通してアプリをダウンロードもしくは購入すると、ゲーム内のバーチャルマネーがもらえる仕組みになっています。普通の広告とは違い、ユーザにもメリットがあるのが特徴的です。

また、広告主にとっても、従来の広告と違いアプリのインストールまでは保証されますし、
有料アプリの場合は広告料を払っても必ず利益が出るため、従来の広告よりも優れています。

以下に、世界のモバイルアフィリエイト広告の現状と今後から引用します。


Tapjoy には広告を掲載するよりも出稿したいという広告主が多いらしい。理由は有料アプリのダウンロードが伸びれば伸びるほど儲かる仕組みが機能しているからである。

<105円のアプリの場合>
Appleの手数料:30%(31.5円)
Tapjoyの手数料:50% (52.5円)
広告主(アプリ)の収入:20% (21円)


2.リワード広告の例


IMG_0372
ゲーム中のメニューからオファースクリーンを開きます。オファースクリーンにはアプリの一覧と、そのアプリの購入もしくはダウンロードによって得られるバーチャル通貨が表示されます。メトセラの場合は魔石です。

IMG_0373
アプリのインストールが終わると、このように報酬を得ることができます。

3.Tapjoyをアプリに組み込む


実際にTapjoyをアプリに組み込むのはすごく簡単です。

(1)ダウンロードしたフォルダにあるTapjoyConnectフォルダをまるごとプロジェクトに追加

window


(2)フレームワークにlibsqlite3.0.dylibとSystemConfigurationを追加

framework


(3)アプリIDをTapjoyのサイトで取得

appid


(4)仮想通貨を設定

currency

1$と仮想通貨の変換レートを設定します。また、デバッグ用端末の端末IDを登録します。

(5)起動時にTapjoyに接続するコードを追加

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

// Override point for customization after application launch.

// Add the view controller's view to the window and display.
[window addSubview:viewController.view];
[window makeKeyAndVisible];

//Tapjoyへ接続する
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tjcConnectSuccess:) name:TJC_CONNECT_SUCCESS object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tjcConnectFail:) name:TJC_CONNECT_FAILED object:nil];
[TapjoyConnect requestTapjoyConnectWithAppId:@"アプリIDを記述"];

return YES;
}

-(void) tjcConnectSuccess:(NSNotification*)notifyObj
{
// NSLog(@"Tapjoy connect Succeeded");
[viewController SetTapjoyStatus:1];
}

-(void) tjcConnectFail:(NSNotification*)notifyObj
{
// NSLog(@"Tapjoy connect Failed");
[viewController SetTapjoyStatus:0];
}

-(void) SetTapjoyStatus:(int)status
{
tapjoy_connected=status;
}

(6)オファースクリーンを開くコードを追加

- (void)openTapjoyOffers{
NSLog(@"open offer");
[TapjoyConnect showOffers:[[UIDevice currentDevice] uniqueIdentifier] withViewController:self withInternalNavBar:YES];
}

(7)ポイントを取得して消費するコードを追加

-(void) getTapjoyPoint
{
if(!tapjoy_connected){
NSLog(@"Failed to connect tapjoy");
NSString *cmd=[NSString stringWithFormat:@"MobileTapjoyGemsCallback(-1);"];
[webView stringByEvaluatingJavaScriptFromString:cmd];
return;
}
NSLog(@"get tap point call");
request_tapjoy_point=1;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getUpdatePoints:) name:TJC_TAP_POINTS_RESPONSE_NOTIFICATION object:nil];
[TapjoyConnect getTapPoints];
}

-(void) getUpdatePoints:(NSNotification*)notifyObj
{
if(!request_tapjoy_point){
NSLog(@"IAP event called bug treat");
return;
}
request_tapjoy_point=0;

NSNumber *tapPoints = notifyObj.object;
NSString *cmd=[NSString stringWithFormat:@"MobileTapjoyGemsCallback(%d);",[tapPoints intValue]];
NSString *ret=[webView stringByEvaluatingJavaScriptFromString:cmd];
if([ret isEqualToString:@"success"]){
//success and spend
[TapjoyConnect spendTapPoints:[tapPoints intValue]];
}
}

request_tapjoy_point変数を使って弾いているのは、IAPの別のイベントからなぜかgetUpdatePointsが呼ばれることがあったためです。

これだけです!デバッグは、WEB上でデバッグ端末の端末IDを入力しておくと、テストオファーが表示されるようになり、そのオファーをタップすることでデバッグポイントを取得、消費することで行います。IAP(In App Purchase)と違って、特別なStoreアカウントでのログインが不要なので楽です。IAPもこの方式になるといいですね。

4.まとめ


Tapjoyは個人でも使える画期的なリワード広告フレームワークです。リワード広告導入アプリ売上の4割がリワード広告経由【ドリコムしらべ】というように、ソーシャルゲームではリワード広告からの売上がかなり大きくなってきています。この流れがモバイルにも広がり、アプリ課金・IAP・リワード広告の三つで収益を最大化するような感じになっていくんじゃないかなと考えています。ちなみに、メトセライズデストラクタでは、次回のver1.2.3から、Tapjoyによるリワード広告をサポートします。

MethuselayzeDestructerメトセライズデストラクタ

Xcode3.2.5でiTunesConnectへアップロードできない問題の対処法

Xcode3.2.5でBuild&ArchiveをしてiTunesConnectにSubmitしようとした所、
 an error occurred uploading to the itunes store
と表示されてアップロードできませんでした。DevForumを見てみると、どうやらXCodeのバグのようです。

Application Loader.appを使うと問題なくアップロードできたという報告があったので試してみました。

具体的に、Build&Archiveの後に開くオーガナイザで"Validate""Share""Submit"と並んでいるボタンから"Share"を選択、"Save to Disk"で.ipaファイルを作成します。その後、Developer/Applications/Utilities/Application Loader.appを開き、先ほど作成した.ipaファイルを指定してアップロードすることで回避します。

iTunesConnectを見るときちんとWaiting For Reviewになっています。よかった!

GameCenterのGKMatchを使ってiOS4.1で通信を行う

connect


三日ぐらい戦ってましたがようやく繋がりました。

まず、OpenGL ESのテンプレートにはViewControllerがないため、leaderboardController等を表示できません。そこで、新しくViewBasedのプロジェクトを作り、従来のOpenGL ESプロジェクトのEAGLView.mやES1Render.mをこのプロジェクトに追加します。そして、ViewController.xibに新規ビューを追加、クラス名をEAGLViewに変えます。あとは、ViewController.mのviewDidLoadで[glView startAnimation];を呼んでから、self.view=(UIView*)glView;と、現在をビューを、追加したEAGLViewで上書きしてやればOpenGLが動きます。

ゲームを起動した直後には、authenticateWithCompletionHandlerでユーザにログインを促します。

- (void) authenticateLocalPlayer
{
[[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:^(NSError *error) {
if (error == nil)
{
}
else
{
// Your application can process the error parameter to report the error to the player.
}
}];
}

Leaderboardを表示するにはGKLeaderboardViewControllerを使います。

- (void) showLeaderboard
{
GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];
if (leaderboardController != nil)
{
leaderboardController.leaderboardDelegate = self;
[self presentModalViewController: leaderboardController animated: YES];
}
}

- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController
{
[self dismissModalViewControllerAnimated:YES];
}

マッチ選択画面を開くには次のようにします。

- (void)hostMatch
{
GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
request.minPlayers = 2;
request.maxPlayers = 2;

GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
mmvc.matchmakerDelegate = self;

[self presentModalViewController:mmvc animated:YES];
}

マッチの構築に失敗したら次のデリゲートが呼ばれます。

- (void)matchmakerViewControllerWasCancelled:(GKMatchmakerViewController *)viewController
{
[self dismissModalViewControllerAnimated:YES];
// implement any specific code in your application here.
}

- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFailWithError:(NSError *)error
{
[self dismissModalViewControllerAnimated:YES];
// Display the error to the user.
}

マッチの構築に成功したら、didFindMatchが呼ばれます。

- (void)matchmakerViewController:(GKMatchmakerViewController *)viewController didFindMatch:(GKMatch *)match
{
[self dismissModalViewControllerAnimated:YES];

match.delegate=self.match_callback;
self.my2Match = match; // Use a retaining property to retain the match.

// Start the game using the match.
NSLog(@"match begin");
}

マッチは今後も使うので、メンバ変数に格納しておきます。

@interface METHUSELAYZEDESTRUCTERViewController : UIViewController {
GKMatch *my2Match;
MatchCallback *match_callback;
}

@property (nonatomic, retain) IBOutlet GKMatch *my2Match;
@property (nonatomic, retain) IBOutlet MatchCallback *match_callback;

このままだとsetterがないと言われるので、ViewControllerで

@synthesize my2Match;

と宣言して、setterをコンパイラに自動生成させます。

マッチに代入するデリゲートは、新規クラスで、GKMatchDelegateを継承して作ります。

@interface MatchCallback : NSObject <GKMatchDelegate> {
bool matchStarted;
}

//match
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state;
- (void)match:(GKMatch *)match connectionWithPlayerFailed:(NSString *)playerID withError:(NSError *)error;
- (void)match:(GKMatch *)match didFailWithError:(NSError *)error;
- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID;

@end

これで、マッチ関連のイベントを受け取ることができます。

プレイヤが参加するごとにdidChangeStateが呼ばれます。match.expectedPlayerCount==0になったら全員がそろったのでゲームを開始します。

- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state
{
NSLog(@"match state called");

switch (state)
{
case GKPlayerStateConnected:
// handle a new player connection.
break;
case GKPlayerStateDisconnected:
// a player just disconnected.
break;
}

NSLog(@"%d",match.expectedPlayerCount);

if (!matchStarted && match.expectedPlayerCount == 0)
{
matchStarted = YES;
NSLog(@"match start");
 }
}

sendDataToAllPlayersで全員に、sendData:data toPlayers:playerIDsで特定のユーザにメッセージを送れます。

[match sendDataToAllPlayers: data withDataMode: GKMatchSendDataUnreliable error:&error];

NSArray *playerIDs=[NSArray arrayWithObject:host_id];
[match sendData:data toPlayers:playerIDs withDataMode:GKMatchSendDataUnreliable error:&error];

送信データはNSData*なので、NSString*と相互変換します。

NSString *str= [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSData *data = [mes dataUsingEncoding:NSUTF8StringEncoding];

メッセージはdidReceiveDataに届きます。

- (void)match:(GKMatch *)match didReceiveData:(NSData *)data fromPlayer:(NSString *)playerID
{
NSString *str= [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}

自分のプレイヤのIDはGKLocalPlayerで取得することができます。

GKLocalPlayer *lp = [GKLocalPlayer localPlayer];
NSString *id=lp.playerID

マッチに参加しているプレイヤのIDはmatch.playerIDsに入ります。ただし、match.playerIDsは自分は含まれません。ですので、クライアントサーバモデルにしてプレイヤの中からホストを決定する場合は、playerIDが一番小さいのとかにするとよさそうです。

なお、GKMatchでは、稼働中のマッチへの乱入はできません。

-----------------------------------------------
参考までにメトセラのコードです。
MatchCallback.mm
MatchCallback.h
METHUSELAYZEDESTRUCTERViewController.m

iOS4.1のGameCenterレビュー

ついにiOS4.1が正式公開されました!一番の目玉はiPhoneのゲームをソーシャル化するGameCenterです。そこで、現在、エイバースで開発中のメトセライズデストラクタiPhone版を使って、レビューを行ってみます。

1

GameCenterに対応ゲームがインストールされると、GameCenterからアプリ管理画面を開くことができるようになります。アプリ管理画面には、次の三つのメニューがあります。

・ランキングを表示するLeaderboards
・実績を表示するAchievements
・最近プレイしたユーザを表示するRecently Played

2

Leaderboardsでは、カテゴリごとにランキングを表示することができます。カテゴリはアプリごとに異なります。開発者はiTunesConnect上でカテゴリを登録し、AppleのGameCenterサーバへスコアを送ることで、自動的にランキングが作られる仕組みになっています。

3

カテゴリを選択すると、フレンド別と全ユーザ別のランキングが表示されます。スコアと同時に、上位何%のプレイヤかも表示されます。

4

Archievementsは、Xboxの実績や、PS3のトロフィーのようなものです。ゲームのステージをクリアするなど、指定された条件をクリアすることで、実績が解放され、ポイントを獲得することができます。開発者は、iTunesConnect上でArchieveを登録します。本当は???にしておいて、実績獲得後に名前表示をしたいのですが、そういったことはできないです。獲得するまで実績の存在そのものを表示しないHiddenモードにはすることができますが、タイトルの変更はできないみたいです。また、512x512の実績画像が必須になります。

gametop

GameCenter対応ゲームを起動すると、”お帰りなさい”と表示されます。

met

マルチプレイは、ゲームからメニューを開きます。

multiplay

すると、このようにマッチメイクする画面が表示されます。ここのUIは結構特殊で、下のオートマッチングというのはクリックできないです。右上の今すぐプレイを選択すると、GameCenterが自動的にプレイ相手を探索してマッチメイクします。右下の参加依頼を選択すると、フレンドをゲームに招待することができます。

GameCenterの一番の魅力は、この招待です。今までのオンラインゲームでは、ユーザがゲームを起動しようと思うまでオンラインプレイできない、いわば能動的なものだったのですが、GameCenterではゲームを起動していなくても招待が届きます。受動的なゲームへの参加が可能になったことで、MO系ゲームがヒットする土壌ができたように思えます。

まだゲーム用の必要最低限の機能しか無いGameCenterですが、今後、プレイヤー情報と、アバターと、コミュニティもしくはゲームごとの掲示板が入ってくれば、かなり魅力的なSNSになりそうです。In App Purchaseで個人でもアイテム課金できるので、SAPとしては結構おいしい市場な気がしますね。

ちなみに、シミュレーションではSandBoxに入れるけど、実機では”このゲームはGameCenterに認識されません”と表示されてSandBoxに入れない場合は、一度アプリを消去してから、再度インストールするとうまく動きます。GameCenterの詳細は、iOS Dev Centerのページが詳しいです。

ではでは。

iPhoneのOpenGLで文字を書く場合の高速化法

前回の記事のようにglTexSubImage2Dでテクスチャに文字を書いてそのテクスチャを描画すれば、OpenGLで文字を書くことができます。しかし、文字列を描画する毎にglTexSubImage2Dを呼び出した場合、1フレームで描画する文字列の種類が増えてくるにつれて、実機では耐えられない重さになります。

実際、iPhoneシミュレータではフォント描画vsトライアングル描画の描画時間が0.1:0.9ぐらいなのですが、実機で動かすと0.5vs0.5と、フォント描画にもの凄く時間がかかることが分かります。これは、iPhoneシミュレータで使っているNVIDIAのアーキテクチャと、iPhoneで使っているPowerVRのアーキテクチャの違いに起因します。

NVIDIAのアーキテクチャ(というか普通のアーキテクチャ)だと、受け取った描画命令は順次実行され、Zバッファとの評価の結果、隠面になるものは、後の描画命令で上書きされます。つまり、裏に隠れる部分も描画しています。

しかし、PowerVRの場合は、独特のタイルベースのアーキテクチャとなっており、前もって描画命令をかなりの数キャッシュしておいて、ピクセルごとにどの描画命令が一番前面に来るかを計算し、実際に見えるピクセルだけを描画します。これにより、実際に見えるテクスチャだけを読みにいくため、モバイル端末等のバス幅の限られたデバイスでも高速に動作しているのです。

このように、PowerVRの特徴は、多くの描画命令をまとめて処理する構造にあります。しかし、glTexSubImage2Dが呼び出された場合、glTexSubImage2D以前に呼び出された描画命令で使われるテクスチャまで更新される可能性があるため、一度、glTexSubImage2D以前の描画命令を全て完了して、フレームバッファに絵を作る必要があります。

普通のアーキテクチャの場合は、描画命令は順次実行しているので、途中で描画をフラッシュしても大きな影響はないのですが、PowerVRの場合は違います。PowerVRは、もしも全命令をバッファリングすることができれば、1ピクセルに対して一回の描画だけで全ピクセルを完成させてしまえます。しかし、ここにglTexSubImage2Dが入るだけで、1ピクセルに対して二回の描画が必要になります。一気に二倍遅くなるわけです。

ということで、AppleのOpenGLES_ProgrammingGuideには下記の注釈があります。

---------------------------------------------
現在のところ、すべてのiPhoneハードウェアはタイルベースの遅延レンダラを使用 しています。このレンダラは、glTexSubImageとglCopyTexSubImageの呼び出しの際に特にコスト がかかります。詳細については、「タイルベースの遅延レンダリング(TBDR)」 (53 ページ)を参 照してください。

タイルベースの遅延レンダリング( TBDR)
PowerVR SGXはTBDR(Tile Based Deferred Rendering:タイルベースの遅延レンダリング)と呼ばれる 手法を用いています。レンダリングのためにOpenGL ESコマンドを送信すると、PowerVR SGXはある 程度の量のレンダリングコマンドが蓄積するまでレンダリングを保留し、蓄積されたコマンドを1 回のアクションで実行します。フレームバッファは複数のタイルに分割されており、シーンは1つ のタイルにつき1回描画されます。各タイルでは、タイル内で見えているコンテンツだけが描画さ れます。遅延レンダラの主な利点は、より効率的にメモリにアクセスできることです。レンダリン グをタイルに分割することによってGPUはフレームバッファからのピクセル値を効率的にキャッシュ できるため、デプステストやブレンドの効率が向上します。
遅延レンダリングのもう1つの利点は、GPUがフラグメントを処理する前に非表示のサーフェスを削 除できることです。表示されないピクセルは、テクスチャのサンプリングやフラグメント処理を実 行せずに破棄されます。したがって、GPUがシーンをレンダリングするために実行しなければなら ない計算量が大幅に削減されます。この機能の効果を最大限に高めるためには、シーンのできるだ け多くの部分を不透過なコンテンツで描画して、ブレンド、アルファテスト、およびGLSLシェーダ でのdiscard操作の使用を最小限に抑えるようにします。非表示のサーフェスの削除はハードウェア によって実行されるため、アプリケーションは全面から背面までのジオメトリを並べ替える必要が ありません。

遅延レンダラの下での操作の中には、従来のストリームレンダラの場合よりもコストがかかるもの もあります。先に説明したメモリ帯域幅と計算量の節約は、大きなシーンを処理する場合に最も効 果があります。小さいシーンのレンダリングを要求する(または、シーンのフラッシュを避けるた めにリソースを複製する)ようなOpenGL ESコマンドをハードウェアが受け取ると、レンダラの効 率は大幅に低下します。
たとえば、アプリケーションがglTexSubImageを呼び出してフレームの中央にあるテクスチャを更 新する場合、レンダラは更新後のテクスチャと以前のテクスチャの両方を同時に保持する必要があ るかもしれず、アプリケーション内のメモリ使用量が増加します。同様に、フレームバッファから ピクセルデータを読み込もうとすると、その前のコマンドがフレームバッファを変更する場合はそ れらが実行されている必要があります。
---------------------------------------------

ということで、文字を書くたびにglTexSubImageを呼び出していると、一気にFPSが低下します。メトセラの場合、20種類の文字列を同時に描画するあたりで、FPSが20から5程度に低下しました。

妥当な解決策は、glTexSubImageの呼び出し回数を減らすことです。具体的に、該当フレームで描画する文字列を全てリストアップしておき、フレームの最初で一つのテクスチャに描画してしまいます。同時に、各文字のテクスチャ上での位置を記憶しておきます。実際の文字列の描画では、テクスチャのuv座標で文字を選択します。これによって、glTexSubImageの呼び出し回数が、文字列の数から、一気に1回まで激減します。メトセラの例では、20から5に低下していたFPSが、15まで3倍程度改善しました。

課題は、512x512ピクセルの空間の有効利用ですが、とりあえずは文字列によってy座標を変えるだけで実装しました。この場合はフラグメンテーションとか考えなくていいのでかなりお手軽です。

int y=m_texture[m_pre_draw_select].height-size.height-m_pre_draw_offset;
UIColor *color=[UIColor colorWithRed:r/255.0f green:g/255.0f blue: b/255.0f alpha:1.0f];
[color set];
[text drawInRect:CGRectMake(0,y,sx,size.height) withFont:m_font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];
m_pre_draw_offset+=size.height;
m_size_x[m_pre_draw_cnt]=size.width;
m_size_y[m_pre_draw_cnt]=size.height;
m_pre_draw_cnt++;

こんな感じで、drawInRectするY座標を変えながら描画していくだけです。将来的には、もっとかっこいいアルゴリズムで位置配置したいですね。

ということでPowerVRではglTexSubImageの呼び出しを最小限に抑えるのがオススメです。

componentsSeparatedByStringとcomponentsSeparatedByCharactersInSetの速度比較

現在、Webkitからディスプレイリストを文字列で送っていて、その文字列の分割が重かったので、componentsSeparatedByStringとcomponentsSeparatedByCharactersInSetの速度を比較してみました。

componentsSeparatedByStringは
 NSArray *dl_split = [dl componentsSeparatedByString:@";"];
と実装し、componentsSeparatedByCharactersInSetは
 NSCharacterSet *spr = [NSCharacterSet characterSetWithCharactersInString:@";"];
 NSArray *dl_split = [dl componentsSeparatedByCharactersInSet:spr];
と実装しています。

結果は以下です。先頭の数字は分割対象の文字の長さです。

iPhoneシミュレータでの結果
2750byte ByString:0.000597sec ByCharater:0.000430sec(28% differ)
2750byte ByString:0.000576sec ByCharater:0.000433sec
2750byte ByString:0.000602sec ByCharater:0.000520sec

iPhone4での結果
2886byte ByString:0.006587sec ByCharater:0.004440sec(33% differ)
2854byte ByString:0.006602sec ByCharater:0.004221sec
2854byte ByString:0.006603sec ByCharater:0.004208sec

iPhone3GSでの結果
2747byte ByString:0.008921sec ByCharater:0.005547sec(39% differ)
2747byte ByString:0.008956sec ByCharater:0.007997sec
2747byte ByString:0.008944sec ByCharater:0.007529sec

単一文字で分割する場合、componentsSeparatedByCharactersInSetを使った方が30%程度高速なようです。

iPhoneのOpenGLで文字を書く

OpenGLにはフォント描画命令が無いので、テクスチャに文字を描き、そのテクスチャをポリゴンに貼って描画することで、文字列の描画を実現します。

テクスチャに直接文字を書くことはできないので、CPU側に確保したメモリ領域に文字を描画し、そのメモリ領域をVRAMに転送することで、テクスチャに文字を書きます。

メモリ領域に文字を書くには、NSString*のdrawInRectを使います。メモリ領域からテクスチャへの転送には、glTexImage2Dを使います。

ただし、テクスチャを毎回glTexImage2Dで新規確保すると重いので、最初に一度だけglTexImage2Dでテクスチャを確保して、以降はglTexSubImage2Dで転送だけを行います。

以降の説明では、以下の構造体を使っています。


//確保したテクスチャを格納
struct DynamicTextureStruct{
GLuint id;
int width;
int height;
int original_width;
int original_height;
};

//文字列の描画先のメモリを格納
struct FontTextureMipmap{
struct DynamicTextureStruct texture; //テクスチャ情報構造体
CGContextRef _context; //コンテキスト
GLubyte* data; //テクスチャのRGBA実データ
};

//文字列描画用のフォント
UIFont *m_font; //文字の描画に使うフォント


データの流れは次のようになります。


初期化
(1)文字列描画用のメモリ領域を確保してglTexImage2Dでテクスチャを確保
(2)GLubyte* dataへの文字列描画コンテキストCGContextRef _contextとフォントUIFont *m_fontを確保
ゲームループ
(3)NSString*のdrawInRectでGLubyte* dataに文字を描画
(4)glTexSubImage2Dでテクスチャに転送


(1)VRAMにテクスチャを生成します。


//テクスチャサイズを定義する
int s=FONT_TEXTURE_MIPMAP_SIZE[i];
m_p_font_texture->texture.width=s;
m_p_font_texture->texture.height=s;

// テクスチャを作成する
glGenTextures(1, &(m_p_font_texture->texture.id));

// テクスチャをバインドする
glBindTexture(GL_TEXTURE_2D, m_p_font_texture->texture.id);

// テクスチャの設定を行う
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);

//テクスチャのRGBAデータの配列を確保する
m_p_font_texture->data = (GLubyte *)malloc(m_p_font_texture->texture.width * m_p_font_texture->texture.height * 4);

//テクスチャデータをVRAM上に転送し領域を確保する
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_p_font_texture->texture.width,m_p_font_texture->texture.height,0, GL_BGRA, GL_UNSIGNED_BYTE, m_p_font_texture->data);


(2)文字描画用のコンテキストを確保します


//文字描画用のコンテキストを作成する
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
m_p_font_texture->_context = CGBitmapContextCreate(m_p_font_texture->data, m_p_font_texture->texture.width, m_p_font_texture->texture.height, 8, m_p_font_texture->texture.width * 4, colorSpace, kCGImageAlphaPremultipliedLast);

//フォントを確保する
m_font = [UIFont systemFontOfSize:14];


(3)文字列を描画します


//実際の描画サイズを取得
CGSize size=[text sizeWithFont:m_font constrainedToSize:CGSizeMake(sx,512) lineBreakMode:UILineBreakModeWordWrap];
m_p_font_texture->texture.original_width=size.width;
m_p_font_texture->texture.original_height=size.height;

//文字画像は上下反転しているので描画時にUV反転する
//また、クリッピングエリアも反転するので注意

// 文字を描画する
memset(m_p_font_texture->data,0,m_p_font_texture->texture.width*m_p_font_texture->texture.height*4);
UIGraphicsPushContext(m_p_font_texture->_context);
UIColor *color=[UIColor colorWithRed:r/255.0f green:g/255.0f blue: b/255.0f alpha:1.0f];
[color set];
[text drawInRect:CGRectMake(0,m_p_font_texture->texture.height-m_p_font_texture->texture.original_height,sx,m_p_font_texture->texture.original_height) withFont:m_font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];
UIGraphicsPopContext();


(4)文字をテクスチャに転送します


// テクスチャをバインドする
glBindTexture(GL_TEXTURE_2D, m_p_font_texture->texture.id);

// テクスチャを更新する
glTexSubImage2D(GL_TEXTURE_2D, 0, 0,m_p_font_texture->texture.height-m_p_font_texture->texture.original_height, m_p_font_texture->texture.width,m_p_font_texture->texture.original_height, GL_RGBA, GL_UNSIGNED_BYTE, m_p_font_texture->data);
}


後はこのテクスチャを描画すればOKです。

尚、glTexSubImage2Dで転送する画像の横幅は、最初に確保したテクスチャ領域の横幅になります。CPU->VRAMへの転送は結構遅いので、複数のサイズの文字描画用テクスチャを用意しておいて、文字サイズに応じて使うテクスチャを切り替えるとよいかと思います。

----------------------------------------
2011/4/19追記
続編がiPhoneのOpenGLで文字を書く場合の高速化法にあります。

また、ソースコードを
http://www.abars.biz/blog/FontTexture.h
http://www.abars.biz/blog/FontTexture.mm
に置きましたのでどうぞ。
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING