CADisplayLinkというの知らなかったのでメモ

今度転職するのに備えて現在iOSリハビリ中です 汗。

少し前にリリースしたiOSアプリに簡単なgifアニメーションを再生しようと思ってライブラリ探していてとりあえずFlipboardのFLAnimatedImageというのに辿り着いた。

Flipboard/FLAnimatedImage · GitHub

でソースみてたのですが、CADisplayLinkというのが今まで見たことなかったものがあったので少し調べてみたのでそのメモ。

CADisplayLinkとは

CADisplayLinkクラスのAppleのリファレンスを日本語に訳してくれてるサイトが以下。

CADisplayLinkクラス | Second Flush

まあこれ見ればもう使い方とかは十分なんですがねw
簡単にいえば、画面が更新されたる度に処理をしたいときに使うもの、って認識でいいと思う。

NSTimerでええんでないの??

で、それってNSTimerでやれることじゃない?という話になると思う。
そこで検索してみると以下の記事が参考になった。

Technical Q&A QA1385: Driving OpenGL Rendering Loops

これはCVDisplayLink(Core Video)についての話だけど気にしなくてOKなはず。

要はパフォーマンスの話で、NSTimerだと設定した時間毎に処理を行おうとするため(タイマーが発火したことにより行われている1つ前の処理がまだ終わっていないにも関わらず、次のタイマーが発火して次の処理を行おうとする)処理がどんどん重たくなっていくが、CVDisplayLink(及びCADisplayLink)はそういったところを換算してくれる、ということだと思う。

使い方

まずQuartzCoreをimportする。

#import <QuartzCore/QuartzCore.h>

でCADisplayLinkのインスタンスをdisplayLinkWithTargetメソッドで生成する。この際に、描画更新毎に呼ばれるセレクターを設定する。
また、生成されたCADisplayLinkのインスタンスをNSRunLoopのメインループに追加してあげる。

CADisplayLink *_displayLink;
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

あとはセレクターを書いてあげればOK

- (void)displayDidRefresh:(CADisplayLink *)displayLink
{
    NSLog(@"called displayDidRefresh");
}

最後に忘れてならないのは、このままだとずっとアプリケーション起動中に処理が走ってしまうので、invalidateしてあげること。

- (void)dealloc
{
    [_displayLink invalidate];
}

Toolkit for CreateJSでパブリッシュするとアニメーションがずれるときがある件

デザイナーさんがつくったFlashアニメーションを、Toolkit for CreateJSを用いてCanvas用にパブリッシュするという業務がちょいちょい発生するのですが、パブリッシュされたものを再生してみると、アニメーションがずれている現象が時々みられました。

アニメーションがずれるというのは例えば、最終フレームで謎の待ち時間が発生してから先頭フレームに戻ってループしたりだとか、タイムライン上では同時に表示されるはずのないオブジェクトが同時に表示されたりです。

ずれがみられたflaファイルをよーくみると、不要な空白フレームがありました。

f:id:ushisantoasobu:20140625224101p:plain

でこの空白フレーム消してやると正しいアニメーションが再生されました。


パブリッシュされるアニメーションのjsファイルのdiffをとってみると、

f:id:ushisantoasobu:20140625224551p:plain

Tweenのwaitの値がおかしくなってるようにみえます。
他こういった話の情報見当たらなかったので不安ですが、念のためメモ。

Toolkit for CreateJSのスプライトイメージ化のフローを検討する (2)

前回のエントリで書いた方法で大方問題ないかなと思ったのだけどまだ駄目だった 汗。

スプライトイメージをそれぞれの画像として用いるためにトリミングしようとするところで、

(lib.hoge = function() {
     this.initialize(img.hoge);
}).prototype = p = new cjs.Bitmap();
p.nominalBounds = new cjs.Rectangle(0,0,64,64);

Bitmapクラスのコンストラクタに渡すのは"imageOrUri"ということで(こちら参照)、
スプライトイメージをcanvasのtoDataUrlメソッドでbase64化したものを使おうと試みた。
が、Android標準ブラウザでエラーが。。。

SecurityError: DOM Exception 18

ちなみにAndorid標準ブラウザでコンソールログ表示する方法は以下
http://www.dprog.info/javascript/about_debug/Android端末のデフォルト・ブラウザでJavaScriptのデバッグをする方法 | design programming

これは便利・・・助かる。


エラーの内容的にクロスドメインまわりかと思い、以前書いたエントリ同様、CORSの対応入れてみたのですがダメだった・・・。

stackoverflowに答えを求めると、

javascript - Android Browser only: canvas.toDataURL throws Uncaught Error: SecurityError: DOM Exception 18 - Stack Overflow

こんなのがあった。

ふむ、全く同じ問題を抱えていて、でもxhrのresponseTypeを'arraybuffer'にすればいけそうとのこと!
だがPreloadJSでいけるのかは不明。次回はそこを試してみる~。

そもそも・・・

このフロー根本的に見直したほうがいいのかとも思った。以下2つのことも調査したい。

  • Flashにスプライト画像をぶち込む

Flashにスプライト画像を読み込んで、それにマスクをかけていってやる方法もあると同僚から提言いただいた。

デザイナーがアニメーションを作成するメリット|1 pixel|サイバーエージェント公式クリエイターズブログ

この方法だと、ちょっと画像追加したくなった~って度にスプライト画像修正するコストが高いと個人的には思うのですがどうなんだろう??

  • Toolkit for CreateJSで吐きだされるjsをもっとごにょごにょ

Toolkit for CreateJSで吐きだされるjsの

(lib.hoge = function() {
     this.initialize(img.hoge);
}).prototype = p = new cjs.Bitmap();
p.nominalBounds = new cjs.Rectangle(0,0,64,64);

の部分をうまいこと変換してしまうというのも手かも。



殴り書きで恥ずかしい・・・。

Toolkit for CreateJSのスプライトイメージ化のフローを検討する (1)

以前書いたToolkit for CreateJSのブログにて、画像のスプライトイメージ化のフローをまだ確立できていないと書いたのだけど、少し進展(?)あったので書いておく。

TexturePackerのコマンドラインツールでスプライトイメージ生成

自分はTexturePackerを使用しているのですが、コマンドラインツールもあるのでそれでやってしまう(TexturePacker起動せずともコマンドでスプライトイメージの生成ができる)。

TexturePacker - Create Sprite Sheets for your game!

まず環境変数のPathを設定してあげて(Windows7 汗)、でコマンド一覧のページが見当たらなかったので

TexturePacker --help > help.txt

みたいな感じでヘルプ書き出したものをみることにした。
で、そこに

Examples:

  TexturePacker assets
        creates out.plist (cocos2d) and out.png from all png files in the 'assets' directory
        trimming all files and creating a texture with max. 2048x2048px

  TexturePacker --data main-hd.plist --format cocos2d --sheet main-hd.png assets
        same as above, but with output files main-hd.plist, main-hd.png

  TexturePacker --scale 0.5 --max-size 1024 --data main.plist --format cocos2d --sheet main.png assets
        creates main.plist and main.png from all files in assets
        scaling all images to 50%, trimming all files and creating
        a texture with max. 1024x1024px
  ...

とわかりやすいサンプルもいくつか載っているのでそれ真似て、

TexturePacker --data %hoge%.json --format easeljs --sheet %hoge%.png images

こんな感じでやることにした。
これで"images"フォルダにある画像を1画像にまとめた"hoge.png"というスプライトイメージと、それぞれの画像の位置情報を含んだ"hogehoge.json"というファイルを生成できる。
ちなみに中身はこんな感じ。

{
"images": ["hoge.png"],
"frames": [

    [2, 2, 173, 105],
    [245, 2, 94, 92],
    [177, 2, 66, 101],
    [390, 2, 49, 81],
    [341, 2, 47, 85],
    [444, 2, 66, 75],
    [444, 79, 38, 31],
    [341, 89, 9, 20]
],
"animations": {
   
        "bear_body":[0],
        "bear_face":[1],
        "bear_legL_B":[2],
        "bear_legL_F":[3],
        "bear_legR_B":[4],
        "bear_legR_F":[5],
        "bear_tail":[6],
        "bear_eye":[7]
},
"texturepacker": [
        "SmartUpdateHash: $TexturePacker:SmartUpdate:d37e132c295fc2a6046c3ecd15380d7e:c648458664f2c206d585b54f0a060629:b395f7e60e6f5b6593be31184330173a$",
        "Created with TexturePacker (http://www.texturepacker.com) for EaselJS"
]
}

アニメーションのjsファイルにjsonのデータをぶち込む

スプライトイメージ化はできたけど代わりにjsonファイルが増えてしまって、通信の回数はもちろん減らしたほうがいいし、それをさらに読み込むつくりにするのも面倒だったので、jsonファイル(というかオブジェクト)をアニメーションのjsファイルの中に無理やりぶち込むことにした。

なかなかダサいのかもですが、こんなファイルをnodeでつくってみました。

//ファイルシステム
var fs = require('fs');

//対話
var readline  = require('readline');
var rl = readline.createInterface({
     input: process.stdin,
     output: process.stdout
});

rl.question("ファイル名を入力してください?\n", function(fileName) {
     rl.close();

     //スプライトイメージのjsonファイルを読み込む
     fs.readFile('./' + fileName + '.json', 'utf8', function (err, text) {

          //読み込みエラー時
          if(err){
               console.log('"' + fileName + '.json"というファイルが見つかりません');
               return;
          }

          //jsonにパース
          var data = JSON.parse(text);
         
          //不要なプロパティ消去
          delete data.images;
          delete data.texturepacker;

          //jsonを文字列に戻す
          var jsonStr = JSON.stringify(data);
         
          //アニメーションjsファイルを読み込む
          fs.readFile('./' + fileName + '.js', 'utf8', function (err, text) {

               //'var p; // shortcut to reference prototypes'という文字列をトリガーに
               //json(オブジェクト)をインジェクションする
               text = text.replace( 'var p; // shortcut to reference prototypes',
                                    'var p; // shortcut to reference prototypes\n\nlib.jsonInfo=' + jsonStr + ';');

               //ファイルに書き込む
               fs.writeFile('./' + fileName + '.js', text , function (err) {
                    //読み込みエラー時
                    if(err){
                         console.log(err);
                         return;
                    }
               });
          });
     });
});


アニメーションのjsファイルを見てみると、

var p; // shortcut to reference prototypes

lib.jsonInfo={"frames":[[2,2,173,105],[245,2,94,90],[177,2,66,101],[390,2,49,81],[341,2,47,85],[441,2,66,75],[441,79,38,31],[341,89,9,20]],"animations":{"bear_body":[0],"bear_face":[1],"bear_legL_B":[2],"bear_legL_F":[3],"bear_legR_B":[4],"bear_legR_F":[5],"bear_tail":[6],"bear_eye":[7]}};

// library properties:
lib.properties = {
     width: 640,
     height: 640,
...

こんな感じになってくれる。

これでサーバから読み込む必要があるものは画像(スプライトイメージ)とjsファイル(アニメーション)だけになった。

上記2つの処理はコマンド1発でできるのでそんな悪くはないのかと思っているけど、まあもっといい方法いくらでもあると思うので教えていただけたらめっちゃ助かります。

"グランブルーファンタジー"の「画質の設定」はいいUXの気がする

同僚がすごい楽しいと言っていたので、Cygamesの"グランブルーファンタジー"をやってみた。

GRANBLUE FANTASY | グランブルーファンタジー(グラブル) - Mobage by DeNA


中身の話は置いておいて、タイトルにもある通り「画質の設定」について。
このゲームではユーザが画質を「軽量版」「標準」「高画質」から選択することができる
(ちなみに演出設定は「軽量」「通常」から、BGMやSEなどの音関連も「OFF」「低音質」「標準」から選択できたりとできる)。
まあそういったもの用意されてるのが最近では当たり前だったりするのかなぁ。
もっと他社サービス研究しないと・・・。

これは何のためかというと、もちろんユーザエクスペリエンス(UX)の1つで、
高スペックのスマートフォンを利用しているユーザはもちろん高画質の画像で楽しんで、
低スペックのスマートフォンを利用しているユーザは画質は落ちるけどその分通信の待ち時間を減らすことでより快適にプレイしてもらうという意図だ。


実際にある画像について、そのピクセル数とファイルサイズをみてみると、

  • 【軽量版】320 * 470px, 54.88KB
  • 【標準】 480 * 705px, 108.16KB
  • 【高画質】640 * 940px, 175.45KB

といった具合だ。

自社のサービス含めてだと思うけど、高スペックの端末でストレスなくプレイできる範囲でできるだけ高画質の画像を用意すればもうそれでOKだという考えになりがちだと思うので、グランブルーファンタジーの「ユーザのが画質を選択できる」というのはいいUXを提供していると思う。

それに対する開発者視点でのコストとしては

  • 画像サーバの圧迫
  • 画像を複数サイズ用意するという作業コスト
  • 画像のサイズを指定する仕組みをつくること(そんな大変じゃない)

くらいだと思う(他にもなんかあるかな?)


The average web page has grown 151% in just three years

この記事にもあるように、webサイトにおけるファイルサイズの画像が占める割合はほぼ半分であり、グランブルーファンタジーのようなゲームにおいてはその割合はもっと多いだろう。
よって画像の読み込み時間をいかに少なくするかは重要課題である
(ネイティブアプリとかであれば、アプリケーションのローカル領域にキャッシュとかもできるけど)。

そんな中でグランブルーファンタジーの画質の設定はいい試みだと思ったので、ふとこんなこと書いてしまいました。

なぜ起きるんだろう→"Resource interpreted as Image but transferred with MIME type text/html"

変なところでハマったのでメモ。

htmlのsrc属性について。今更。。。

あるとき開発環境で頻繁にエラーが出るようになった。サーバ側のエラーログみるとトークンエラーだった。
原因がなんと↓こんなこと書いてあったからだった。

<img src="#"/>

おそらく動的にsrc属性の中身を変えるうえで

<a href="#"/>

といったhrefでよく見る書き方を真似したんだと思う。
これで何が起きるか・・・Chromeのコンソールみると、

Resource interpreted as Image but transferred with MIME type text/html: "http://ushisantoasobu.jp/test/list/show".

こんなのが表示されていた。

いまローカルにあるhtmlに上のコード追加してブラウザで表示してみても同じエラー出た。
でなんとこれ、エラーメッセージ通り「画像として解釈されたけど、htmlとして転送します」ということで、再度ページ自体をロードするんす・・・(赤矢印部分)。

f:id:ushisantoasobu:20140305012509p:plain

これのお陰でトークンが再度サーバ側で新しく発行されてトークンエラーが出ていた。

恐ろしいソース。。。

ということで

<img src=""/>

こうした。こうすると、ブラウザによっては空のURLにアクセスして無駄な通信走らすみたいな記事もみかけたけど、それ以上に、AndroidのWebViewで表示したときに動かなくなってしまった。。。

ここ慌てて修正してしまったので(リリース間近だった)、どのように動かなくなったのかをイマイチちゃんと調べきれてない。今度ちゃんと調べてみよう(はじめのと同じパターンなのかも)。

結局は

<img />

そもそもsrc属性は後で追加するということで問題なくなった。

1*1ピクセルの透明の画像を入れるということをしている同僚もいるようだ。

今更こんなことにハマってなんか嫌だなふむ。。




(余談)

早速使ってみた。

はじめてToolkit for CreateJS使ってみての所感

業務ではじめてToolkit for Createjs使いました。
その所感というか、ハマったところ・気をつけなければならないことも多々あったのでメモ。

flash内の画像ファイル名やインスタンス名には気をつけよう

flash内で画像ファイル名やインスタンス名が日本語だと、パブリッシュで生成されるjsファイル内に2バイト文字が混ざることになる。これは最悪。
今回はデザイナーさんにflashでアニメーションをつくってもらったあとにそのことに気づいてしまったので、お手数ながたリネームしてもらったということがあったので、以後デザイナーさんにToolkit for Createjsでアニメーションをつくってもらうときは注意が必要。

画像読み込みについて

画像のファイル数は比較的多くなりがちになるのだと思うけど、

地獄の“大開発者”養成ブログ — クレイジー極まりないToolkit for CreateJSのワークフローをどうにかしよう その1 問題を整理しよう

でも書かれているように、パブリッシュ時に画像をスプライトイメージにまとめてくれるわけもなく、画像の読み込みがそのままだと重くなってしまう。このスプライトイメージ化のフローはまだ自分も確立できていない。スプライトイメージにする余裕がないときは、せめて画像の読み込みは並列にしてあげたほうが絶対いいと思う。

loader.setMaxConnections(6);

jsファイルは遅延読み込みの考慮も

アニメーションの記述が細かく書かれたjsファイル容量は結構重いので、場合によっては遅延ロードするのが良いかも。自分の場合だと、必ずしもそのアニメーションが表示されるというわけではなかったので(ユーザが条件を満たしているときのみ表示されるアニメーション)遅延読み込みしています。

画像パス

画像パスについてはパブリッシュされたjsファイル内に相対パスで記述されている。で自分の場合は画像サーバとjsファイルを置くサーバが異なるので、そこのパスを修正しなければならないのだけど、jsファイルを直接書き換えるのはNG。そうなると修正入ったときにまた書き換えなければいけないから。なのでパブリッシュされたHTML側で以下のような記述を書くようにした。

var list = test.properties.manifest,
	len = list.length;
for(i = 0; i < len; i++){
	src = "ushisantoasobu/test/" + list[i].src;
	list[i].src = src;
}

書き方はともかく、jsファイルは一切手をつけないこと。これ大事。

バージョン注意

これ一番ハマった。。といっても問題が起きるケースは稀かもしれない。
まずToolkit for Createjsでパブリッシュすると、生成されたHTML内でcreatejs関連のjsファイルをスクリプトタグで読み込んでいる(こちらはFlash Professional CCでパブリッシュしたもの)。

<script src="http://code.createjs.com/easeljs-0.7.0.min.js"></script>
<script src="http://code.createjs.com/tweenjs-0.5.0.min.js"></script>
<script src="http://code.createjs.com/movieclip-0.7.0.min.js"></script>
<script src="http://code.createjs.com/preloadjs-0.4.0.min.js"></script>

で、問題が起きたケースとしては、プロジェクトで使っているcreatejsがあって、それをToolkit for Createjsでつくったアニメーションと同時に使用しなければならない、そのときにcreatejsでバージョンの差異があるときだ。

<script src="/js/libs/createjs.min.js"></script>
## 実際にあった例、当プロジェクトで使用しているcreatejsは少し古く、
## "http://code.createjs.com/createjs-2013.05.14.min.js"にあたるもので
## "EaselJS 0.6.1, TweenJS 0.4.1, SoundJS 0.4.1, PreloadJS 0.3.1"という構成

createjsではバージョンの互換が効かないことが多々あって、上記の例では一方のバージョンに寄せるだけだと一方が動かなくなる。

Flash側でcreatejsのバージョンの調整はできないらしい。

"createjs"という名前空間自体を変更してパブリッシュすることもできるので、それでうまくいけるのかなと思って試してみたのですがうまくいかなかった・・・(ここできるよ、という方はご教授ください!)

で結局どうしたかというと、Flash Professional CS6でパブリッシュしたときに同じcreatejsのバージョンだったので、なんとか事なきを得たという感じだったのだけど・・・ここらへん怖い。



以上、Toolkit for CreateJS関連の情報もそんなないしあまり使われてないのかなぁ〜。