2016年4月17日日曜日

新作ゲームの解像度検討メモ(256x240にした経緯)

今までスマホ前提ということで、SUZUKI PLANのゲームは基本的にQVGA縦(240x320)という解像度を使ってきたが、PC用ゲームの解像度としてQVGA縦というのは、横画面でフルスクリーンにした時に微妙だと思っていたりする。

NOKOGI RiderのWindows版はそれでも(スマホがメインということで)QVGA縦でゴリ押ししたが。(NOKOGI Riderは)縦置きディスプレイでのフルスクリーンにも対応していたというのもある。しかし、今回はMacにも対応する。Macで縦置きディスプレイというのはあまり聞かない。デスクトップ版のMacの事情はよく分からないが、少なくともMacBookで外付けディスプレイを使わずに縦置きというのは無い。

という訳で、横置きディスプレイでのフルスクリーンをプライマリに想定する必要がある。その場合、解像度をどうしたものかが悩ましい。とりあえず「正方形でいいや」と考え 256x256 で作り始めてみたものの、ひとつ重大な問題を思い出す。

WindowsのDirectX9のDirectGraphicでオフスクリーンサーフェイス描画をする場合、ビュー領域の拡大を自前処理でやらなければならない。DirectGraphicだと、サーフェースロックをしてメモリ領域にダイレクトに点を打つ(※この場合GPUを使わない)ローレベルなインタフェースがある(OpenGLには無い)。

ピクセル単位で描画する描画方式(VGSやエミュレータ全般で一般的な描画方式)の場合、GPU経由で描画すると、GPUはシステム・メモリ間のデータのやりとりが絶望的に遅いため、リアルタイムに描画内容が変わるラスタ形式グラフィックのようなものはGPUで描画できない。

GPUは使っていないので、回転、拡大縮小、半透明といった加工ロジックを全て自前実装する必要がある。ゲームを作ったことがない人だと「こんなオーパーツ的な技術は今でも必要なのか?」と疑問に思うかもしれないが、例えばメニューやメニューの上に載せる描画(文字など)、体力ゲージ的なものの表示などで(フル3Dのゲームだったとしても)割とよく使う。

レトロチックな純2Dのゲームの描画であれば、GPUを一切使わずこの方式だけで描画するのが最も高速になる。だから、VGSはそれで描画する前提のレンダリング機能しか実装しないようにしている。(スマホゲームの場合、GPUを使うゲームと比べて消費電力量や本体の温度上昇を大幅に抑えられるメリットがあったのでかなりうま味だったと思う)

そして、Windowsの場合、フルスクリーンの解像度は640x480などの固定サイズにする必要がある(昔は320x240でフルスクリーンにするmode-xというものがあったが、今は無いので最低解像度は640x480)。

少し前置きが長かったが、解像度を256x256にした場合の問題とは、縦のピクセル(256)を綺麗に480に拡大できないということ。等倍じゃないと pixel by pixel で拡大ができないので画面が汚くなるし、せめて間引き方式で拡大できる1.5xとかでないと拡大縮小時に浮動小数点数演算が必要になる関係で(自前拡大に要する)処理コストも増えてしまうので不味い。

という訳で縦のピクセル数は 240 (480÷2なので綺麗 & 低コストで拡大できる)にして、横は256ピクセルにしてみた。これをMacでフルスクリーン表示した場合のキャプチャが下図。
Mac + 解像度256x240 で フルスクリーン にした キャプチャ

妙に既視感のある解像度だな...

と思ったら、256x240 ってファミコンの(表示上の)解像度と同じか。

ファミコンの解像度は正確には256x256の正方形で、縦は上下8ピクセルが切れている。その切れている間がBLANK割り込み期間で、その間にスプライトの情報などを更新するとチラつき無くスプライトが動かせたりする感じだったような気がする。(ファミコンのコードを最後に書いたのが結構昔の話なので、若干記憶が曖昧だが、ファミコンだと非描画系処理を非BLANK時、描画系処理をBLANK時という具合に実装する必要があって苦労したような記憶がある)

スーファミの場合も確か同じ解像度があったような。ただし、SFCには解像度に幾つかの種類があって、どちらかといえば 512x240 の方がよく使われていたかな(だからスーファミには縦より横の方が木目が細かいグラフィックのものが多い: )。

Windows(基本的にVGSベース)と比べて横長のMac(640x400というPC-9801相当の解像度がベース)でも左右の帯が概ね気にならないので、コレで良い感じか。

2016年4月10日日曜日

iOSとOSXでのvsyncの取り方の違い

私がiOSアプリを開発した時に最初に詰まったのが、vsync(垂直同期)の発生タイミングを検知する方法で、色々調べて(確かmameか何かのソースを読んで) CADisplayLink を使えば良いことが分かりました。

CADisplayLinkの使い方は大雑把に説明すると下記のような感じです。

MyView.h
@interface MyView : UIView
@property CADisplayLink* displayLink;
@end

MyView.m
- (id)initWithFrame:(CGRect)frame {
  if ((self = [super initWithFrame:frame])!=nil) {
    ...CALayerの初期化など...
    _displayLink=[CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
  }
  return self;
}

こういう風にすることで、vsyncの発生間隔の都度setNeedsDisplayが呼び出されて、60fpsでViewの描画がされるようになります。

そんなのNSTimerで良いのではないか?と思われるかもしれませんが、NSTimerで1/60秒間隔でsetNeedsDisplayをコールすると、若干画面時にブレ(ガクガク感)みたいなものが発生してしまうのでダメです。
何故、NSTimerではダメなのかというと、垂直同期の呼び出し間隔は常に一定ではなく、17ms, 17ms, 16ms 17ms, 17ms, 16ms... という風になっているので、常に一定間隔でコールしているとズレが生じてしまう為ではないかと思われます。

そして、OSX用のゲームを開発しようとした時、CADisplayLinkはUIじゃないからOSXでもそのまま使えるよね(※)などと考えていたのですが、甘かった。OSXの場合、CVDisplayLinkを使う必要があります。
iOSとOSXでは同じCocoaアプリでも作り方が微妙に違います。例えば、iOSのUIViewはOSXには無くて、代わりにNSViewがあったりとか。プレフィックス「UI」のクラスはiOS用で「NS」はOSX用という感じです。名前が違うので当然機能も微妙に違います。ですが、CADisplayLinkはプレフィクスがUIじゃなくてCA(CoreAnimation)だから、OSXでもそのまま使えるよねなどと考えていた時期が私にもありました。
CVDisplayLinkを用いた場合の実装は下記です。

MyView.h
@interface MyView : NSView
@property CVDisplayLinkRef displayLink;
@end

MyView.m
- (id)initWithFrame:(CGRect)frame
{
  if ((self = [super initWithFrame:frame]) != nil) {
    ...CALayerの初期化など...
    CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
    CVDisplayLinkSetOutputCallback(_displayLink, MyDisplayLinkCallback, (__bridge void *)self);
    CVDisplayLinkStart(_displayLink);
  }
  return self;
}

static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *context)
{
  [(__bridge VGSLayer *)context performSelectorOnMainThread:@selector(vsync) withObject:nil waitUntilDone:NO];
  return kCVReturnSuccess;
}

- (void)vsync
{
  [self.myLayer setNeedsDisplay];
}

若干気をつけないといけないのは、CVDisplayLinkSetOutputCallbackで指定できるコールバックの呼出し規約がC(Objective-cではない)ということですかね。(なので、若干面倒なブリッジが入っています)

あと、上記の例では省略していますが、何度も使い回されるタイプのViewの場合、確保したディスプレイリンクの停止(CVDisplayLinkStop)や解放(CVDisplayLinkRelease)が必要です。
詳細は下記を参照。
https://developer.apple.com/library/mac/documentation/QuartzCore/Reference/CVDisplayLinkRef/

2016年4月9日土曜日

【osx/cocoa/obj-c】フルスクリーンにする

OSXアプリをフルスクリーンにする方法についてのメモです。

まず、OSX/Cocoaアプリケーションのプロジェクト(storyboardあり)を作成した状態で、Window Controller → Window の Attribute を設定
・「Full Screen」に「Primary Window」を設定
・他は適当です
ウィンド⇔フルスクリーンの切り替えが必要な場合はもう少し色々と設定する必要があるが、とりあえずフルスクリーンのみで動くアプリ=ゲームを作る前提ならこんな感じではなかろうかと。

次に、ViewController.m に以下を追加。

- (void)viewWillAppear
{
    [self.view.window.contentView enterFullScreenMode:self.view.window.screen withOptions:nil];
    [self.view.window makeFirstResponder:self.vram];
    [NSCursor hide];
}

【やっていること】
・コンテントView(self.view.window.contentView)に対して enterFullScreenMode を発行
・何故かフルスクリーンにすると first responder が外れ、キー入力が受け付けられなくなった(キーを押すと「ポン」という警告音が鳴る)ので makeFirstResponder を発行しておく
・self.vram = self.viewの子View(ゲームのメインView)
・なお、Viewの構造としては以下のような感じです
 - self.view : 画面一杯の真っ黒なNSView
 - self.vram : ゲーム映像を表示するNSView
・マウスカーソルを消しておく

これでフルスクリーンになれました。

なお、NSViewのenterFullScreenMode以外にも、NSWindowのtoggleFullScreenでもフルスクリーンになれたのですが、そっちの方のやり方だと失われた first responder を復活させる方法がよく分からなかった(恐らく、フルスクリーン化が完了したことを検知するデリゲートとかで makeFirstResponder するのではないかと思うのですが、そのやり方が分からなかった)ので、NSViewの方法を採りました。

画面描画については、iOSと同じ方法(CALayerを使う方法)でできるので割愛。
UIKitを使っている場合は簡単にはできませんが、CALayerはOSXもiOSもだいたい同じ仕組みなので、自前の描画エンジンで画面描画をするなら、CALayerだけで描画する(or OpenGLで描画する)のが良いんじゃないかと思います。

2016年4月3日日曜日

熱→喉

3/31木曜日の夜
・突如として高熱(39.4度ぐらい)が出る
・ほぼ寝れず

4/1金曜日
・医者へ行き念のためインフル検査→陰性
・熱以外に症状は無いので抗生物質等はもらわずとりあえず解熱剤を貰っておく
・昼アベレージ38.x度、夜アベレージ39.xぐらい
・とりあえず会社休んでおく
・慣れてきたので寝れる(ただし体力不足で1時間おきに目覚める)

4/2土曜日
・昼アベレージ37度台に下がる
・夜平熱ぐらいに戻る
・代わりに喉が痛くなる
・喉が痛すぎて一睡もできず

4/3日曜日(早朝)
・喉が痛すぎてツライ
・AM8:00まで耐え、最寄のドラッグストアへ
・閉まっていた(東京都心の休日の朝は遅い...)
・とりあえず、喉に良さそうなものをコンビニで(お茶、はちみつ、のど飴)
・お茶ですら割とツライ
・いい歳したおっさんがはちみつをダイレクトに食すのは滑稽である
・そういえば今日、先日注文したiPhone SEが届くらしい(これで勝つる?)
・Siriに「喉が痛い」と聞いてみようなどと考えながら、はちみつを舐めているおっさんは、実際かなりキモい。

【cocos2d-x】set design resolution size が機能しなかった原因

前回の記事で終了だとあんまりなので、少し追いかけてみました

まず、createWithFullScreenの実装:

bool GLViewImpl::initWithFullScreen(const std::string& viewName)
{
    CGRect rect = [[UIScreen mainScreen] bounds];
    Rect r;
    r.origin.x = rect.origin.x;
    r.origin.y = rect.origin.y;
    r.size.width = rect.size.width;
    r.size.height = rect.size.height;
    return initWithRect(viewName, r, 1);
}

途中経過は色々と省略していますが、上記のような感じで画面のサイズがフルスクリーンサイズって事ですね。これは、cocos2d-xのインタフェース仕様上それしか手段が無いので、まぁそうなりますよね。本当はデベロッパーが使いたいスクリーンサイズの近似値にすべきじゃないか?と思ったけど。その方が描画コストが浮きそうなので。Macなら最小640x400のはず(PC-9801と同じですね)。

で、次に問題のsetDesignResolutionSize。
まずは 全体grep でアタリをつけます。

$ grep -R designResolutionSize cocos2d/cocos
cocos2d/cocos/platform/CCGLView.cpp:        && _designResolutionSize.width > 0 && _designResolutionSize.height > 0)
cocos2d/cocos/platform/CCGLView.cpp:        _scaleX = (float)_screenSize.width / _designResolutionSize.width;
cocos2d/cocos/platform/CCGLView.cpp:        _scaleY = (float)_screenSize.height / _designResolutionSize.height;
cocos2d/cocos/platform/CCGLView.cpp:            _designResolutionSize.width = ceilf(_screenSize.width/_scaleX);
cocos2d/cocos/platform/CCGLView.cpp:            _designResolutionSize.height = ceilf(_screenSize.height/_scaleY);
cocos2d/cocos/platform/CCGLView.cpp:        float viewPortW = _designResolutionSize.width * _scaleX;
cocos2d/cocos/platform/CCGLView.cpp:        float viewPortH = _designResolutionSize.height * _scaleY;
cocos2d/cocos/platform/CCGLView.cpp:    _designResolutionSize.setSize(width, height);
cocos2d/cocos/platform/CCGLView.cpp:    return _designResolutionSize;
cocos2d/cocos/platform/CCGLView.cpp:    _designResolutionSize = _screenSize = Size(width, height);
cocos2d/cocos/platform/CCGLView.cpp:        return _designResolutionSize;
cocos2d/cocos/platform/CCGLView.cpp:        return Vec2((_designResolutionSize.width - _screenSize.width/_scaleX)/2, 
cocos2d/cocos/platform/CCGLView.cpp:                           (_designResolutionSize.height - _screenSize.height/_scaleY)/2);
cocos2d/cocos/platform/CCGLView.h:    Size _designResolutionSize;
cocos2d/cocos/platform/ios/CCGLViewImpl-ios.mm:    _screenSize.width = _designResolutionSize.width = [glview getWidth];
cocos2d/cocos/platform/ios/CCGLViewImpl-ios.mm:    _screenSize.height = _designResolutionSize.height = [glview getHeight];
cocos2d/cocos/platform/ios/CCGLViewImpl-ios.mm:    _screenSize.width = _designResolutionSize.width = [eaglview getWidth];
cocos2d/cocos/platform/ios/CCGLViewImpl-ios.mm:    _screenSize.height = _designResolutionSize.height = [eaglview getHeight];

んー...
正しい値が設定されていれば正常に動きそうですね。
CCGLView.cppという部分はプラットフォーム非依存っぽいので。

以下の _scaleX, _scaleY の算出直後にログを入れてみますか。
cocos2d/cocos/platform/CCGLView.cpp:        _scaleX = (float)_screenSize.width / _designResolutionSize.width;
cocos2d/cocos/platform/CCGLView.cpp:        _scaleY = (float)_screenSize.height / _designResolutionSize.height;

算出直後に
printf("_scaleX=%f, _scaleY=%f", (double)_scaleX, (double)_scaleY);
という感じでログ出力を入れてみたところ、
_scaleX=3.333333, _scaleY=3.333333

ん?
結果は正しそう。
320x240 の 3.3333倍 ということはだいたい1066x800。
スクリーンサイズはおそらく1280x800(イメージはretinaだから2560x1600)なので。
しかし、先ほどの画像(下図)のフォント部分のサイズは3.3333倍されてはいない。
フリーハンドで寸借したところ大体210x80ぐらい?
(retinaだから1/2なので105x40ぐらい?)
フォント表示の処理はこんな感じ。

    auto sprite = Sprite::create("res/spfont.png");
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
    this->addChild(sprite, 0);

単純にスプライトとしてフォント画像を読み込んで配置しているだけですね。
気になったので、念のため visibleSize と origin の値をログに出してみた。

visibleSize.width=320.000000
origin.x=0.000000
visibleSize.height=240.000000
origin.y=0.000000

うん、問題ありません。
余談ですが、座標基準が中央のゲームエンジンってゲーム作る人のセンスが欠落したダメなヤツだと思っています。実際そういうエンジン(というかCocoaのAPIの事ですね)を使ったことが何度かありますが、回転が楽なことを除き全ての点で左上基準と比べてゲームの作り易さに欠けると思うので。ゲーム以外だと中央基準の方が作り易いと思うので、ゲームを作らない人がどちらかというとゲーム開発向けのAPIを作るとこういう齟齬仕様が生まれ易いのではないかと想像している(ゲームの話に戻すと、アーケードゲームのスプライトに回転機能がつき始めた頃、回転機能を思いっきり駆使した意欲作みたいなゲームがありましたが、使えるとしたらそういうゲームぐらいではないだろうか。ゲーム自体はあまり面白くなかったけど。なお、3Dになってからのことは分からないです)

んー...?
試しにsetScaleX, setScaleYで3.3333倍にズームしてみようか。
拡大された(驚愕)
つまり、setDesignResolutionSizeというのは、「座標系は合わせてやる」(拡大/縮小までしてやるとは言っていない)という機能なのか。

これは酷い。
まぁ、OpenGLをゼロから調べて使うよりは楽かもしれませんが、この手のクソ仕様は今後ももりもり出てきそうで怖いな。

という訳で、cocos2d-xのモジュールは、メインのコントロール部分とOpenGL部分のモジュール以外は全部削除して使うことにして、可能な限りcocos2d-x標準機能は使わないようにするのがベストかな。(あと、ラッパーライブラリみたいなものは必須ですね)

これならcocos2d-xを使わない方が楽なので、cocos2d-xはやはり不採用ということで。

【cocos2d-x】そして、伝説へ(set design resolution size does not work)

前回記事で、ドヤ顔でcocos2d-x導入編の記事を書いてみた訳ですが、解像度の設定がどうも意図した通りに動いていないらしい。
前回記事の図
glviewを
glview = GLViewImpl::createWithFullScreen("iblock");
で作成して、
glview->setDesignResolutionSize(320, 240, ResolutionPolicy::SHOW_ALL);
で、320x240を引き延ばして画面いっぱいに表示という風にしたつもりです。
が、どう見てもQVGAの荒さが感じられない...

試しに、320x240でWindowモードにしてみる。
glview = GLViewImpl::createWithRect("iblock", Rect(0, 0, 320, 240));

すると、下図のような感じになりました。
分かり易くするために、真ん中に表示されている画像を 128x48 ピクセルの SUZUKI PLAN FONT に変更しています。

では、この状態でもう一度フルスクリーンモードにしてみると...

うぉ!
文字がちっさいw
つまり、setDesignResolutionSizeが効いていないっぽい。
setDesignResolutionSizeが効いていれば、ウィンドウモードで起動した時と同じ比率で引き伸ばされた画像が表示される筈なので。

cocos2d-xだと、Macのフルスクリーンサポートは未だ不完全ってことなのかな。
cocos2d-xのソースを追っかけるのも怠い。
こういうことがよくあるからC++じゃなくてCで書いて欲しかった。C++は書く分には楽で、保守スパンが短いアプリとかならC++で書くべきだと思っているけど、他人が書いた未知のコードを追いかける時は、Cの方が楽なので。(アプリの保守スパンが短いというのは、最近の事情を勘案すると語弊があるかもしれませんが、銀行などの基幹系システムなどで利用されている20年もののプロダクトコードとかと比べればまだまだ浅漬け程度かなと)

cocos2d-xとは別の策を探るべきかな。
スマホ向けライブラリということで、PC向きに使うには未だ時期尚早だったのかもしれません。(Webで見つかるコードが割と互換性が無いものが多い点から見て、まだAPI仕様がかなりドメスティックに変わっているようなので、まだまだ安定していないようですし)
あと、コンパイルが遅すぎるのも怠い。

という訳で私のcocos2d-x利用の模索はここで一旦終了です。
早かったなぁー

やっぱり、VGSでPCサポートを完璧にするのが楽かな...

2016年4月2日土曜日

【cocos2d-x】導入〜解像度設定

この前、HTML5で作った Invader Block を cocos2d-x を使って PC に移植して、もうちょっと演出周りを凝らせてみて、Steamで販売してみようかと思い立つ。

VGSを使っても良いんですけど、PC用のVGSはサンドボックス用途での利用しか考えていなくて、WindowsはともかくOSXとLinuxもサポートしようとなると、現状の実装(SDL)では割と不満があります。(デフォルト状態で動かないのが一番の難点かと思います)

かといって、OSXとLinuxのネイティブ描画も凝らせようとするとそれだけで半年ぐらい浪費してしまう。という訳で、面倒なグラフィックス周りは既存ライブラリの力を借り、音声周りだけは vgs-bgm-decoder を使う方向で作るのがベターかなと。

そして、C/C++で書けて、Windows, Mac OSX, Linuxをサポートしているライブラリはcocos2d-xの一択ではないかと思います。(本当はランタイムはC++じゃなくてCの方が好みですが、CだとVGS以外に無さそうなので、この際、C++でも(C++13ぐらいまでのものなら)いいやと妥協)

という訳で、まずは導入。
git clone https://github.com/cocos2d/cocos2d-x.git
cd cocos2d-x
python download-deps.py
git submodule update --init
多分、全部で300MBぐらいあります。
デカイ...
気長にダウンロード。

そして setup & プロジェクト作成(iblockとかsuzukiplanといった部分は良しなに)
./setup.py
cocos new iblock -p com.suzukiplan.iblock -l cpp -d ~/cocos2d-x
cd ~/cocos2d-x/iblock
そして、OSX向けにコンパイル&実行してみる。
cocos run -p mac
これがまた長い...

が、無事 Hello, World! が起動。

ここまでなら README.md に書いてある通りのことだから、ブログに書いても芸が無いので、PCゲーム向けということで 320 x 240 (QVGA) の解像度 で フルスクリーン にしてみます。

WindowsでDirectXを使ったことがある人なら分かると思いますが、それだけでも実は結構面倒くさかったりするのですが、2行変更しただけでイケました。



変更箇所1: 44行目(createWithFullScreenを使う)
glview = GLViewImpl::createWithFullScreen("iblock");

変更箇所2: 59行目(setDesignResolutionSizeで解像度を設定)
glview->setDesignResolutionSize(320, 240, ResolutionPolicy::SHOW_ALL);

左右が少し黒く切れていますが、これは想定通り。
※Macの解像度は1280x800(Retinaなら2560x1600)なので、QVGAをアスペクト比を保った状態で画面いっぱいに引き伸ばすと、左右に余白が生まれる