トーテムポーる

トーテムポーるはじめました

国内のTVゲームは60fpsないし30fpsで動いているというのは御存じだろうが、これはTVディスプレイの垂直同期周波数 (VSYNC rate) が 国内では 60 Hz (for NTSC) だからに他ならない。 ついでにヨーロッパなどでは50 Hz (for PAL and SECAM) となっている。 この周期に合わせてゲーム状態を更新し、画面を描画しなければならないのである。

これは、ゲーム機の内部に大して正確でもない時計しかなかったら偉いことになるが、実際にはそうはならない。 なぜなら一般的なコンシューマーゲーム機などではVSYNC割込みという仕組みが用意されているからである。 VSYNC割り込みはVSYNCのタイミングに合わせてゲームのメインルーチンを呼ぶので、ゲームプログラムはこれにしたがってゲームを進行すれば良いからである。

それはそうと、VSYNCはあまり気にする必要が無いPCゲームでも便宜上この更新間隔に合わせてゲームが進行される。 この方がコンシューマー機への移植にも便利という背景も幾分かはあるように思える。 ただ、少なくともここで大事なのはPCゲームでも一定間隔でゲームの状態の更新と描画を行わなくてはならないということだ。

Project VimuktiではSDLを軸にクロスプラットホームなゲームを作ろうと考えているのだが、SDLでどうやってVSYNC同期処理的な動作を実現するかというのを今回は考えてみる。

まずはじめにクロスプラットホームを意識しないでコーディングする場合を説明し、次にSDLでの実現のしかたを検討する。

まず、SDLを使わない場合のVSYNC同期処理的なロジックの*BSD上での記述のしかたを以下に記す。

#<sys/time.h>
#<signal.h>

void al_handler(int sig){} // dummy alarm handler (nop)

int main(){
    struct itimerval itval;
    struct itimerval itoval;

  
    // set timer interval
    itval.it_interval.tv_sec = 0;
    itval.it_interval.tv_usec = 33000; // (1sec=1000000usec)/30fps
    itval.it_value.tv_sec = 1;
    itval.it_value.tv_usec = 0;

    signal(SIGALRM, al_handler);
    setitimer(ITIMER_REAL, &itval, &itoval);
    
    while(1){
        sleep(999999); 
        //
        // フレーム処理
        //
        }
    }
}

一見するとsleep()を長い時間するように見えるが、実際にはSIGALRMがinvokeされるとsleep()が解除されてdummy alarm handler (al_handler())に飛び、なにもせずにフレーム処理に移るので正確に33msec刻み、つまり、30fpsで動作させることが出来る。 今までのVimuktiのクライアントはプロトタイピングの段階だったのでとりあえずこの方法で記述されていた。

SDLの場合も、同様のしくみで実現できそうだと考えたのだが、よく考えたらこの動作はプラットホーム間で互換性が無い。 例えば同じUNIX-like OSでもSoralisなんかは、少し違う動作をする。(al_handler()が呼ばれない。) そういうことなので、この方法は使えないらしく、Using SDL: Timersのような方法が用いられる。 これは、多少名の知られたSDLを使用したゲーム用開発用ライブラリHome of Ferzkopp - SDL_gfxでもfps管理のために次のフレーム開始時間までを待つための関数が用意されており、これがraine-0.50.0/source/sdl/SDL_gfx/SDL_framerate.c - Google Code Searchのような実装方法を用いていることからも伺える。

また、SDLを用いた他のアプローチではうまくいかない場合が多い。 例えばSDL_AddTimer()を用いる方法が二つと、実際のVSYNC同期を用いる方法が考えられるが、これらのいずれもうまくいかない。

一つ目はSDL_AddTimer()のハンドラ関数でフラグを立て、それをチェックしてメインルーチンの実行を行う方法。 フラグは、大域変数を用いるよりは、SDL_USEREVENTを用いた方が良いだろう。 どうやら、SDL_WaitEventがあるのでそのような動作を真似る関数は作れそうな気がする。 しかし実際には、この方法はゲームには使用できない。 そもそもこの関数自体ががゲームには役に立たないのだ。 何故か。 それはこの関数の実装がmpeg4ip-0.9.6.1/lib/SDL/src/events/SDL_events.c - Google Code Searchのように、eventが無ければSDL_Delay(10)で待ってから、もう一度チェックするという風になっているからである。 SDL_Delay()はSDL_Delayにもあるように移植性の観点からこれ以上の精度で引数を与えてもその精度を実現出来る保証はされない。 これは30fps, 33msec per frame 以上で動作しなくてはならないゲームにとっては致命的である。 よって、この方法ではうまくいかない。

次に、SDL_Addtimer()のハンドラ関数から、実行する場合を考えてみる。 これは、実現不可能というわけではない。 しかし、実装が複雑になりすぎる。 なぜなら、SDL_Addtimer()のハンドラ関数はSDL_AddTimerにあるように、別のthread上で同一時間に重複してに呼ばれる可能性があるからだ。 これはゲームの処理が1frame分の時間に収まらなかったときに発生する。 このような状況でthread-safeを徹底するのは大変難しくバグの温床になるのでこの方法は用いるべきではない。

最後に、実際のVSYNC同期行う方法がSDLにも無くは無いということを紹介する。 ひとつはSDLが描画エンジンとしてDirectXを検出した場合。 もうひとつはSDL上でOpenGLによる描画をSDL_GL_DOUBLEBUFFERをセットして行う場合。 この場合、裏バッファと表バッファの切替時にSDLはVSYNCを待ってバッファの切替えを実行する。 ただし、これらはドライバの設定・バージョンに完全に依存することになる。 そのためこの方法を用いてゲームを同期的に進行することは期待すべきではない。

以上の考察からSDLでクロスプラットホームを意識した開発を行う場合には時間を見ながらプログラムが自前で適切な時間を待つという方法を用いなければならないことが分かった。

出先からzero3で書き込んでるので間違ってる部分があると思います
コメントで添削していただけると助かります
また、タイトルにあるとおりメモ書き程度の内容ですので、詳しい内容は別のエントリーで後日改めて書かせていたできます

マウスカーソルがある位置に表示されているものの判定を行うにはいくつかの方法がある

たとえば
(void) glRenderMode ( GL_SELECT );
を使う方法

このモードを設定した状態でオブジェクトのレンダリングを行うとview座標を中心にした二次元クリッピング領域を設定しその中にあるオブジェクトを取得することができる

ただしこの方法は、前述の通り、表示用以外にレンダリングを行う必要があり、一回あたりのレンダリングのコストが高くつく場合には向いていない

ほかにgluUnproject()を使う方法がある

GLdouble fx ,fy ,fz ;
gluUnProject(mousePointX, mousePointY,
0.0, modelMatrix,
projMatrix,viewport,&fx,&fy,&fz);

これは引き数に与えたview座標に対応するモデル空間での座標に逆変換する

これはモデル座標に該当するオブジェクトを即座に特定できる場合に有効である

尚、この方法を三次元モデル座標に適用するには

GLdouble fx0, fy0, fz0;
GLdouble fx1, fy1, fz1;
gluUnProject(mousePointX, mousePointY,
0.0, modelMatrix,
projMatrix,viewport,&fx0,&fy0,&fz0);
gluUnProject(mousePointX, mousePointY,
1.0, modelMatrix,
projMatrix,viewport,&fx1,&fy1,&fz1);

の様に gluUnproject() を二回呼び出して、線分 (fx0, fy0, fz0), (fx1, fy1, fz1) に属するオブジェクトを判定する必要がある

このようにgluUnproject()を用いる方法は二次元オブジェクト空間に対して用いるのには向いているが三次元オブジェクト空間に用いるのにはあまり向いていないことが分かる

この考察から次のような方法を用いるのがよいと考える

まずwindow, button and etc... などの二次元的なwidgetsはgluUnproject()を用いて判定する
それにより三次元描画領域にフォーカスがあることが分かった場合はGL_SELECTを用いた判定を行う
このとき、選択可能なオブジェクトのみを最低限の、可能な限りプリミティブなオブジェクトでレンダリングするのが望ましい

当然、二次元的なwidgetsの配置が単純で、素早くwindow内の絶対座標で判別できるときは、わざわざgluUnproject()を用いる必要はない

とりあえずこのように考えたわけだが諸兄の考えはどうだろうか
このブログはmixiの日記へ自動的に内容が同期されるようになってます
そこで思うことが一つ

mixiとブログの内容が同じなのってのはどういうことなのかと

現状の利点というのは、mixiからblogへの集客が望めること、もしくはblogの記事がより多くの人に読んでもらえること(内容如何は別として)
現在のやり方ならmixiの内部の検索にも引っかかるので効率がいい
これは自分のベネフィットを優先させるのには向いている

ここでマイミクのベネフィットを考えてみる
マイミクは日記の更新が取得できる
これはmixiの日記とblogの日記が同じだとすればblogのRSSを購読すればそっちの方が便利だ
RSSを利用する術がどれくらい浸透しているかを考えなくてはならない
もし十分浸透していれば必ずしもこの点はマイミクのベネフィットとなり得ないことになる

問題はmixiユーザーのwebリテラシーのレベルだ
少なくとも2chねらーの平均よりは低い水準であるのは間違いない
類は友を呼ぶのであればRSS readerを常用している人間のマイミクにはそういう人間が集まるのかもしれないが

とりあえずともほかのベネフィットを用意しておかなくてはならないわけで、その辺を考えてみる
たとえばblogの記事以外のmixiオリジナルの日記を用意するとか

そもそもそれがめんどくさいからblogの記事とをmixiの日記に自動同期してるわけだし

そこら辺は割り切るしかないか

もうちょっと考えてみよう

概要

本エントリーではSVKという分散バージョン管理システムを用いてSubversion のローカルとリモートのrepositoryの管理を効率的に行う方法について記述する。

目的

過去のrevisionと互換性が維持されづらい大幅な変更を一度に行う場合次のような需要が生まれる。

作業時に頻繁にcommitを行いたい。 問題がでた場合に、問題の無いrevisionまで戻したい。

目的の作業が達成された場合のみcommitを行いたい。 中途半端にコンパイルが通らないようなソースコードはcommitしたくない。

これらを同時に行うには、作業時の頻繁なcommitはlocalのrepositoryに行い、目的の作業が達成された場合のみremote のsource repositoryにcommit出来れば良い。

ここで問題となるのがlocal repository と remote (source) repository のmergeである。 Subversion ではsvn mergeでこれを行う事が出来るが、revision指定を行わなくてはならないなど繁雑である。

そこでsvkというツールを用いる。 このツールはSubversionのしくみを用い、さらに次のような特徴を加えるものである:

  • Offline operations like "checkin", "log", "merge".
  • Distributed branches.
  • Lightweight checkout copy management (no .svn directories).
  • Advanced merge algorithms, like star-merge and cherry picking.
この特徴からsvkを用いれば通常時にはoffline operationsを用いてcommitを行い、必要に応じてremote source repositoryと同期するということがseamlessに行う事が出来る。

svkの使い方

% svk mirror https://sample.com/svn/proj //mirror/proj
% svk sync //mirror/proj
% svk cp //mirror/proj //proj
mirrorでmirroringするrepositoryを設定。 これがlocal mirrorとなる。 syncで同期。 cpでlocal branchを作成。 以後、localな変更はlocal mirroへcommitする。

これらに関連するファイルは file://~/.svk/local をrootとしたSubversion repositoryとして作成される。

Subversion repositoryなのでsvnでcheckout出来る。 svkでcheckoutすることも出来る。

どちらを選ぶかは自由だが、svnの方が立上りが早い。 また、svnでは管理repositoryのエンコードに応じてエディタ (VIM) のエンコードが適切に設定されるが、svkではこれが行われない。 正しいエンコードでコミットメッセージを記述しないとcommit時にエラーがでるので注意が必要である。 svkではコミットメッセージのファイルの保存時にエディターに正しいエンコードを明示的にあたえる必要がある。

% svn co file:///home/username/.svk/local/proj path/to/working/directory
でcheckout. coはcheckoutのalias.

以降はworking directoryで作業を行う。

変更をcommitするには作業ディレクトリで

% svn ci
とする。 commit先はlocal branch となることに注意する。 ciはcommitのalias.

local branchの変更をsource repository (この例では https://sample.com/svn/proj) に反映するには以下のようにする:

% svk push //proj
これにより "local branch" -> "local mirror" -> "source repository" というように変更がcommitされる。

逆に、source repository の変更をlocal branchに反映するには:

% svk pull //proj
とする。 これにより "source repository" -> "local mirror" -> "local branch" のようにrepositoryがupdateされる。

さらにこれを作業ディレクトリに反映するには作業ディレクトリ上で:

% svn update
とする。

参考文献

Dia2Code HomepageというDia a drawing programでかいたUML class diagram からプログラムコードを出力するツールがあります。
C++のソースコードも出力する事が出来ます。
しかしDoxygen 用のコメントまでは付けてくれません。
そこで I hacked a patch to enable to generate C++ source code with Doxygen comments.
patch

変数の引数に対するコメントも出力出来ます。
ただし、400 byte までです。
それ以上だと segmentation fault します。

頭の方のheaderを書き換えてる部分はFreeBSDでコンパイルを通すための物なのでplatform が違う場合は削除してください。

誰かセグらない様にしてくれないかなぁ。
公式にcommitしてもらおうにもこれでは無理だし。

概要

本記事はC++で可変長多次元配列を関数に引数として渡す方法について、関数内でインデックスアクセスをしなければならない場合を二次元配列を例にとって議論する。 また、これに際してBoost C++ Libraries のboost::multi_array のインデックスアクセスに付いても自分が遭遇したトラブルを交えて論じる。

Cのatoi()で変換できないときのチェックはどうやったらいいのかと思ってmanを引いてみた。
% man 3 atoi
結論からいうと
The atoi() function has been deprecated by strtol() and should not be used in new code.
と書いてあったので、atoi()は使わずにstrtol()を使った方が良いらしい。

なぜならば、The function atoi() need not affect the value of errno on an error. とあるように、エラーを返すような仕様にはなっておらずチェックのコードがかけないかららしい。

いずれにせよ、
It is equivalent to:

(int)strtol(nptr, (char **)NULL, 10);

とあるようにstrtol()を間接的に呼び出しているので、atoi()を使うよりもManpage of STRTOLを参考に(こちらはLinuxのman-page)エラー処理のコードをいれて書いた方がよいということのようだ。

一応追記の方に元のman-pageを記載しておきます。
キーショートカット: b - 前のページ n - 次のページ